This commit is contained in:
Ludovic Marie committed 2026-10-27 13:42:59 +01:00
commit 6ec974ab30
34 files changed
+4271 -106

No files matched your search

@@ -1062,6 +1062,8 @@ pub fn main() -> horizon_client::Result<()> {
last_hurt: None,
velocity: Vec3::ZERO,
yaw: 0.0,
sprinting: false,
riding: false,
hero: false,
effects: Vec::new(),
uuid: PLAYER_UUID,
+17
View File
@@ -339,6 +339,18 @@ pub fn mob_to_nbt(m: &Mob, uuid: [i32; 4], raw: Option<&Tag>) -> Tag {
map.insert("Sitting".into(), Tag::Byte(i8::from(m.extra.sitting)));
map.insert("CollarColor".into(), Tag::Byte(m.extra.color as i8));
}
"minecraft:panda" => {
use crate::mobs::panda::GENES;
for (key, gene) in [
("MainGene", m.extra.variant),
("HiddenGene", m.extra.variant2),
] {
map.insert(
key.into(),
string(GENES[usize::from(gene).min(GENES.len() - 1)]),
);
}
}
"minecraft:piglin" | "minecraft:piglin_brute" | "minecraft:hoglin" => {
// Only Piglin.addAdditionalSaveData writes CannotHunt.
if id == "minecraft:piglin_brute" {
@@ -1094,6 +1106,11 @@ pub fn mob_from_nbt(tag: &Tag, types: &HashMap<String, Arc<MobType>>, id: u32) -
m.extra.color =
number(tag.get("CollarColor")).map_or(crate::mobs::tame::RED, |c| (c as u8) & 15);
}
if m.species() == crate::mobs::Species::Panda {
let gene = |key| text(tag.get(key)).map_or(0, |n| crate::mobs::panda::gene_from_name(&n));
m.extra.variant = gene("MainGene");
m.extra.variant2 = gene("HiddenGene");
}
if m.species() == crate::mobs::Species::Wolf {
let health = m.health;
m.extra.owner = crate::save::uuid_from_tag(tag.get("Owner"));
+1
View File
@@ -131,6 +131,7 @@ mod packs;
mod pad;
mod pad_menu;
mod paintings;
mod pandas;
mod passengers;
mod persist;
mod perspective;
@@ -39,6 +39,7 @@ impl Game<'_> {
}
}
self.eye_particles();
self.panda_effects();
self.sprint_dust();
self.effect_swirls();
}
+42 -2
View File
@@ -214,6 +214,8 @@ impl Game<'_> {
last_hurt: self.combat.last_hurt_mob.map(|(id, at)| (Who::Mob(id), at)),
velocity: p.velocity,
yaw: self.yaw,
sprinting: p.sprinting,
riding: self.riding.is_some(),
hero: self.potions.effects.get("hero_of_the_village").is_some(),
effects: self.potions.effects.list.iter().map(|e| e.id).collect(),
uuid: self.level.player_uuid.unwrap_or_default(),
@@ -736,6 +738,7 @@ impl Game<'_> {
self.tick_projectiles()?;
self.tick_fangs();
self.tick_allays();
self.tick_pandas();
self.tick_breeze_effects();
self.tick_sniffer_effects();
self.piglin_pickups();
@@ -1073,6 +1076,19 @@ impl Game<'_> {
}
}
}
MobEvent::Knockback {
target,
power,
x,
z,
} => match target {
Who::Player => self.knock_back_player(power, x, z),
Who::Mob(id) => {
if let Some(m) = self.mobs.iter_mut().find(|m| m.id == id) {
m.knockback(power, x, z);
}
}
},
MobEvent::Shift { target, delta } => {
let d = Vec3::new(delta[0], delta[1], delta[2]);
match target {
@@ -1522,6 +1538,7 @@ impl Game<'_> {
m.ai.aggressive |= aggressive;
}
}
MobEvent::PandaSneezed { mob } => self.panda_sneezed(mob),
MobEvent::ShedScute { mob, at, pitch } => {
// The mob already checked `shouldDropLoot` (a baby, `mob_drops`).
let table = "minecraft:gameplay/armadillo_shed";
@@ -1539,8 +1556,12 @@ impl Game<'_> {
self.post_game_event("minecraft:entity_place", at, ctx);
}
MobEvent::GiftLoot { mob, table, at } => {
// Turtle.ageBoundaryReached: only with mob loot on.
if table == "minecraft:gameplay/turtle_grow" && !self.ui.rules.bool("mob_drops")
// Turtle.ageBoundaryReached and Panda.afterSneeze: only
// with mob loot on.
if matches!(
table,
"minecraft:gameplay/turtle_grow" | "minecraft:gameplay/panda_sneeze"
) && !self.ui.rules.bool("mob_drops")
{
continue;
}
@@ -1779,6 +1800,13 @@ impl Game<'_> {
self.spawn_breeding_orb([p.position.x, p.position.y, p.position.z], xp);
return;
}
// Panda.getBreedOffspring: genes from both parents.
if p.species() == Species::Panda {
horizon_client::mobs::panda::inherit(child, &p, q.as_ref().unwrap_or(&p));
let xp = 1 + self.world.random.next_int_bound(7).unwrap_or(0) as u32;
self.spawn_breeding_orb([p.position.x, p.position.y, p.position.z], xp);
return;
}
// Nautilus.getBreedOffspring: a tame parent's baby is tame too.
if p.species() == Species::Nautilus && p.extra.tame {
child.extra.tame = true;
@@ -1787,6 +1815,10 @@ impl Game<'_> {
}
// Offspring inherit a parent's variant (pigs/cows/chickens) or mixed wool.
child.extra.variant = p.extra.variant;
// Fox.getBreedOffspring: either parent's variant.
if p.species() == Species::Fox && !coin {
child.extra.variant = q.as_ref().map_or(p.extra.variant, |q| q.extra.variant);
}
if p.species() == Species::Sheep {
child.extra.color = p.extra.color;
}
@@ -3091,6 +3123,14 @@ impl Game<'_> {
let creative = self.creative;
// The hand's slot (Player.interactOn passes the hand).
let selected = self.held_slot();
// Panda.mobInteract: bamboo, or a click on a panda lying on its back.
if self.mobs[index].species() == Species::Panda && self.mobs[index].alive() {
let taken = self.interact_panda(index, &item);
if taken {
self.swing_arm();
}
return Ok(taken);
}
if self.interact_zombie_villager(index, &item) {
return Ok(true);
}
+254
View File
@@ -181,6 +181,136 @@ pub(super) fn baby_index(kind: &str) -> Option<u8> {
}
}
/// The variant and look entries of `mob_meta`'s mobs (registry holders, the
/// fish's, the bee's, the copper golem's weathering...), in a match of their
/// own: `mob_meta`'s arms hold these mobs' other entries.
fn mob_variant_meta(
m: &Mob,
hub: &Hub,
meta: &mut Vec<(u8, i32, Value)>,
defaults: &mut Vec<(u8, i32, Value)>,
) {
let (kind, e) = (m.kind.id.as_str(), &m.extra);
let id = |registry: &str, name: &str| hub.registry_id(registry, &format!("minecraft:{name}"));
match kind {
// Axolotl.DATA_VARIANT (after AgeableMob's two).
"minecraft:axolotl" => {
if e.variant != 0 {
meta.push((18, serializer::INT, Value::Int(i32::from(e.variant))));
}
}
// Frog.DATA_VARIANT_ID, Pig's, Cow's, Chicken's and ZombieNautilus's
// (after the entries of their parents): registry holders.
"minecraft:frog"
| "minecraft:pig"
| "minecraft:cow"
| "minecraft:chicken"
| "minecraft:zombie_nautilus" => {
let (index, serializer, registry) = match kind {
"minecraft:frog" => (18, serializer::FROG_VARIANT, "minecraft:frog_variant"),
"minecraft:pig" => (19, serializer::PIG_VARIANT, "minecraft:pig_variant"),
"minecraft:cow" => (18, serializer::COW_VARIANT, "minecraft:cow_variant"),
"minecraft:chicken" => {
(18, serializer::CHICKEN_VARIANT, "minecraft:chicken_variant")
}
_ => (
21,
serializer::ZOMBIE_NAUTILUS_VARIANT,
"minecraft:zombie_nautilus_variant",
),
};
// Each registry's default (`define(DATA_VARIANT_ID, ... TEMPERATE)`)
// stays out of the pairing data, as other default values do.
let name = horizon_client::mobs::finalize::variant_name(m.species(), e.variant);
let name = name.trim_start_matches("minecraft:");
if let Some(v) = id(registry, name)
&& name != "temperate"
{
meta.push((index, serializer, Value::Id(v)));
}
if let Some(v) = id(registry, "temperate") {
defaults.push((index, serializer, Value::Id(v)));
}
}
// Rabbit.DATA_TYPE_ID (after AgeableMob's two) and
// Parrot.DATA_VARIANT_ID (after TamableAnimal's two and its flags and
// owner). The fox's type goes with its flags (#2538).
"minecraft:rabbit" | "minecraft:parrot" => {
let index = if kind == "minecraft:parrot" { 20 } else { 18 };
if e.variant != 0 {
meta.push((index, serializer::INT, Value::Int(i32::from(e.variant))));
}
}
// Bee.DATA_FLAGS_ID (rolling 2, stung 4, nectar 8) and
// DATA_ANGER_END_TIME: the texture shows the nectar and the anger.
"minecraft:bee" => {
let bee = horizon_client::mobs::bee::get(m);
let flags = bee.map_or(0, |b| b.flags);
if flags != 0 {
meta.push((18, serializer::BYTE, Value::Byte(flags as i8)));
}
defaults.push((18, serializer::BYTE, Value::Byte(0)));
let anger_end = bee.map_or(-1, |b| b.anger_end);
if anger_end != -1 {
meta.push((19, serializer::LONG, Value::Long(anger_end)));
}
defaults.push((19, serializer::LONG, Value::Long(-1)));
}
// AbstractFish.FROM_BUCKET takes 16: Salmon.DATA_TYPE (small 0,
// medium 1, large 2; medium is the data's default, so always sent),
// Pufferfish.PUFF_STATE and TropicalFish.DATA_ID_TYPE_VARIANT: the
// pattern's packed id (size, then index << 8), base colour << 16 and
// pattern colour << 24.
"minecraft:salmon" => {
meta.push((17, serializer::INT, Value::Int(i32::from(e.variant.min(2)))));
}
"minecraft:pufferfish" => {
if e.size > 0 {
meta.push((17, serializer::INT, Value::Int(e.size)));
}
defaults.push((17, serializer::INT, Value::Int(0)));
}
"minecraft:tropical_fish" => {
let pattern = e.variant.min(11);
let packed = i32::from(pattern >= 6)
| i32::from(pattern % 6) << 8
| i32::from(e.variant2 & 15) << 16
| i32::from(e.variant3 & 15) << 24;
if packed != 0 {
meta.push((17, serializer::INT, Value::Int(packed)));
}
}
// CopperGolem.DATA_WEATHER_STATE: unaffected 0 to oxidized 3, and it
// goes back down when the golem is scraped.
"minecraft:copper_golem" => {
if e.variant != 0 {
meta.push((
16,
serializer::WEATHERING_COPPER_STATE,
Value::Id(i32::from(e.variant.min(3))),
));
}
defaults.push((16, serializer::WEATHERING_COPPER_STATE, Value::Id(0)));
}
// Ghast.DATA_IS_CHARGING: the shooting texture.
"minecraft:ghast" => {
if e.flag {
meta.push((16, serializer::BOOLEAN, Value::Bool(true)));
}
defaults.push((16, serializer::BOOLEAN, Value::Bool(false)));
}
// WitherBoss.DATA_ID_INV (after the three head targets): the
// invulnerable ticks, the blinking texture and the growing scale.
"minecraft:wither" => {
if e.counter > 0 {
meta.push((19, serializer::INT, Value::Int(e.counter)));
}
defaults.push((19, serializer::INT, Value::Int(0)));
}
_ => {}
}
}
/// A mob's synched data as ServerEntity sends it (only the values that differ
/// from the defaults the client constructs), with vanilla's entry indices:
/// Entity 0..=7, LivingEntity 8..=14, Mob 15, then the subclasses.
@@ -258,6 +388,7 @@ fn mob_meta(
}
let e = &m.extra;
let id = |registry: &str, name: &str| hub.registry_id(registry, &format!("minecraft:{name}"));
mob_variant_meta(m, hub, meta, defaults);
match kind {
"minecraft:guardian" | "minecraft:elder_guardian" => {
// Guardian.DATA_ID_MOVING and DATA_ID_ATTACK_TARGET.
@@ -314,6 +445,18 @@ fn mob_meta(
}
defaults.push((18, serializer::BYTE, Value::Byte(0)));
}
// Fox.DATA_TYPE_ID and DATA_FLAGS_ID (after AgeableMob's two; sleeping 32, ...).
"minecraft:fox" => {
if e.variant != 0 {
meta.push((18, serializer::INT, Value::Int(i32::from(e.variant))));
}
defaults.push((18, serializer::INT, Value::Int(0)));
let flags = (e.ints[0] & 0xff) as u8 as i8;
if flags != 0 {
meta.push((19, serializer::BYTE, Value::Byte(flags)));
}
defaults.push((19, serializer::BYTE, Value::Byte(0)));
}
"minecraft:cat" | "minecraft:wolf" => {
// TamableAnimal.DATA_FLAGS_ID: sitting 1, tame 4.
let flags = i8::from(e.sitting) | if e.tame { 4 } else { 0 };
@@ -342,6 +485,14 @@ fn mob_meta(
meta.push((collar, serializer::INT, Value::Int(i32::from(e.color & 15))));
}
defaults.push((collar, serializer::INT, Value::Int(red)));
// Wolf.DATA_ANGER_END_TIME: the game time the anger ends, -1 when calm.
if kind == "minecraft:wolf" {
if e.anger > 0 {
let end = game_time + i64::from(e.anger);
meta.push((22, serializer::LONG, Value::Long(end)));
}
defaults.push((22, serializer::LONG, Value::Long(-1)));
}
}
"minecraft:horse"
| "minecraft:donkey"
@@ -420,6 +571,29 @@ fn mob_meta(
}
defaults.push((17, serializer::BOOLEAN, Value::Bool(false)));
}
// Panda: UNHAPPY_COUNTER, SNEEZE_COUNTER and EAT_COUNTER (after
// AgeableMob's two), MAIN_GENE_ID, HIDDEN_GENE_ID and DATA_ID_FLAGS;
// the client animates the states from them.
"minecraft:panda" => {
let (flags, counters) = horizon_client::mobs::panda::get(m)
.map_or((0, [0; 3]), |s| (s.flags, [s.unhappy, s.sneeze, s.eat]));
for (index, value) in (18..).zip(counters) {
if value != 0 {
meta.push((index, serializer::INT, Value::Int(value)));
}
defaults.push((index, serializer::INT, Value::Int(0)));
}
for (index, gene) in [(21, e.variant), (22, e.variant2)] {
if gene != 0 {
meta.push((index, serializer::BYTE, Value::Byte(gene as i8)));
}
defaults.push((index, serializer::BYTE, Value::Byte(0)));
}
if flags != 0 {
meta.push((23, serializer::BYTE, Value::Byte(flags as i8)));
}
defaults.push((23, serializer::BYTE, Value::Byte(0)));
}
// PolarBear.DATA_STANDING_ID (after AgeableMob's two): the bear
// rears up; the client animates it.
"minecraft:polar_bear" => {
@@ -470,6 +644,86 @@ fn mob_meta(
}
defaults.push((18, serializer::BYTE, Value::Byte(16)));
}
// Vex.DATA_FLAGS_ID: charging in bit 0 (the arms and the glowing
// texture).
"minecraft:vex" => {
if e.flag {
meta.push((16, serializer::BYTE, Value::Byte(1)));
}
defaults.push((16, serializer::BYTE, Value::Byte(0)));
}
// Creaking.CAN_MOVE, IS_ACTIVE and IS_TEARING_DOWN.
"minecraft:creaking" => {
if e.flag2 {
meta.push((16, serializer::BOOLEAN, Value::Bool(false)));
}
defaults.push((16, serializer::BOOLEAN, Value::Bool(true)));
if e.flag {
meta.push((17, serializer::BOOLEAN, Value::Bool(true)));
}
defaults.push((17, serializer::BOOLEAN, Value::Bool(false)));
if horizon_client::mobs::creaking::tearing_down(m) {
meta.push((18, serializer::BOOLEAN, Value::Bool(true)));
}
defaults.push((18, serializer::BOOLEAN, Value::Bool(false)));
}
// Allay.DATA_DANCING and DATA_CAN_DUPLICATE (the held item is its
// main hand equipment).
"minecraft:allay" => {
if e.flag {
meta.push((16, serializer::BOOLEAN, Value::Bool(true)));
}
defaults.push((16, serializer::BOOLEAN, Value::Bool(false)));
if e.egg_time > 0 {
meta.push((17, serializer::BOOLEAN, Value::Bool(false)));
}
defaults.push((17, serializer::BOOLEAN, Value::Bool(true)));
}
// Axolotl.DATA_PLAYING_DEAD (after DATA_VARIANT).
"minecraft:axolotl" => {
if horizon_client::mobs::axolotl::is_playing_dead(m) {
meta.push((19, serializer::BOOLEAN, Value::Bool(true)));
}
defaults.push((19, serializer::BOOLEAN, Value::Bool(false)));
}
// CopperGolem.COPPER_GOLEM_STATE (after DATA_WEATHER_STATE).
"minecraft:copper_golem" => {
if e.variant3 != 0 {
meta.push((
17,
serializer::COPPER_GOLEM_STATE,
Value::Id(i32::from(e.variant3)),
));
}
defaults.push((17, serializer::COPPER_GOLEM_STATE, Value::Id(0)));
}
// Armadillo.ARMADILLO_STATE (after AgeableMob's two).
"minecraft:armadillo" => {
let state = horizon_client::mobs::armadillo::state(m);
if state != 0 {
meta.push((18, serializer::ARMADILLO_STATE, Value::Id(i32::from(state))));
}
defaults.push((18, serializer::ARMADILLO_STATE, Value::Id(0)));
}
// Frog.DATA_TONGUE_TARGET_ID (after the variant): the entity id plus
// one, 0 for none.
"minecraft:frog" => {
if let Some(target) = e.frog.tongue_target {
let id = i32::try_from(target).map_or(0, |t| t + 1);
meta.push((19, serializer::OPTIONAL_UNSIGNED_INT, Value::Id(id)));
}
defaults.push((19, serializer::OPTIONAL_UNSIGNED_INT, Value::Id(0)));
}
// Goat.DATA_HAS_LEFT_HORN and DATA_HAS_RIGHT_HORN (after
// DATA_IS_SCREAMING_GOAT): both true by default.
"minecraft:goat" => {
for (index, lost) in [(19, e.lost_horns[0]), (20, e.lost_horns[1])] {
if lost {
meta.push((index, serializer::BOOLEAN, Value::Bool(false)));
}
defaults.push((index, serializer::BOOLEAN, Value::Bool(true)));
}
}
// Enderman.DATA_CARRY_STATE.
"minecraft:enderman" => {
if let Some(state) = e.carried {
@@ -87,6 +87,10 @@ pub(super) fn acts_on_player(e: &MobEvent) -> bool {
target: Who::Player,
..
}
| MobEvent::Knockback {
target: Who::Player,
..
}
| MobEvent::Ignite {
target: Who::Player,
..
@@ -383,6 +387,8 @@ mod tests {
last_hurt: None,
velocity: Vec3::ZERO,
yaw: 0.0,
sprinting: false,
riding: false,
hero: false,
effects: Vec::new(),
uuid: [0; 4],
+7 -5
View File
@@ -386,13 +386,15 @@ impl Game<'_> {
owner.use_item_remaining_ticks = 72_000 - mob.extra.using_item;
owner.crossbow_charge_duration = 25;
}
let context = if left {
horizon_assets::item_model::DisplayContext::ThirdPersonLeftHand
} else {
horizon_assets::item_model::DisplayContext::ThirdPersonRightHand
use horizon_assets::item_model::DisplayContext as Context;
// PandaHoldsItemLayer shows the item as it lies on the ground.
let (context, ordinal) = match (mob.species(), left) {
(Species::Panda, _) => (Context::Ground, 7),
(_, true) => (Context::ThirdPersonLeftHand, 1),
(_, false) => (Context::ThirdPersonRightHand, 2),
};
// `updateForLiving`: seeded by the entity id plus the context ordinal.
let seed = mob.id as i32 + if left { 1 } else { 2 };
let seed = mob.id as i32 + ordinal;
let state = self.resolve_item(&stack, context, Some(&owner), seed);
if lit {
self.submit_item_world(&state, t, light, out);
+165
View File
@@ -0,0 +1,165 @@
//! Pandas on the game side: the food they find on the ground and pick up
//! (`Mob.aiStep`, `Panda.pickUpItem`), the bamboo a player gives them
//! (`Panda.mobInteract`), the start a sneeze gives the grown pandas about,
//! and the crumbs and the sneeze a window draws.
use super::{
Game,
pickups::{Collector, Taken},
};
use horizon_client::{
items::ItemStack,
mobs::{Species, panda},
particles::{ParticleOptions, ParticleType},
};
use horizon_core::geometry::Vec3;
impl Game<'_> {
/// `Panda.canPickUpAndEat`'s tag.
fn panda_eats_from_ground(&self, item: &str) -> bool {
self.res
.items()
.item_tags
.get("minecraft:panda_eats_from_ground")
.is_some_and(|tag| tag.contains(item))
}
/// A panda takes what lies at its feet when its mouth is free, and
/// looks for food within 6 and 8 blocks for `PandaSitGoal`.
pub(super) fn tick_pandas(&mut self) {
let griefing = self.ui.rules.bool("mob_griefing");
for i in 0..self.mobs.len() {
let m = &self.mobs[i];
if m.species() != Species::Panda || !m.alive() {
continue;
}
let id = m.id;
let wants = |g: &Self, e: &horizon_client::entities::ItemEntity| {
e.pickup_delay == 0 && g.panda_eats_from_ground(&e.stack.item)
};
// Mob.aiStep: the items within the box grown by (1, 0, 1).
if griefing && m.can_pick_up_loot() && m.extra.main_hand.is_none() {
let reach = m.bounds().inflate(Vec3::new(1.0, 0.0, 1.0));
let taken = self
.dropped
.iter()
.position(|e| wants(self, e) && reach.intersects(e.bounds()));
if let Some(j) = taken {
let e = self.dropped.remove(j);
self.add_pickup(Taken::Item(Box::new(e.clone())), Collector::Mob(id));
panda::hold(&mut self.mobs[i], &e.stack.item, e.stack.count, true);
}
}
let m = &self.mobs[i];
let area = |r: f64| m.bounds().inflate(Vec3::new(r, r, r));
let (near, wide) = (area(6.0), area(8.0));
let food_near = self
.dropped
.iter()
.any(|e| wants(self, e) && near.intersects(e.bounds()));
let food_at = self
.dropped
.iter()
.find(|e| wants(self, e) && wide.intersects(e.bounds()))
.map(|e| [e.position.x, e.position.y, e.position.z]);
panda::set_food(&mut self.mobs[i], food_near, food_at);
}
}
/// `Panda.mobInteract` for a click with `item`; whether the click is taken.
pub(super) fn interact_panda(&mut self, index: usize, item: &str) -> bool {
let creative = self.creative;
let m = &mut self.mobs[index];
if panda::scared(m) {
return false;
}
if panda::get_up(m) {
return true;
}
if !m.kind.food.iter().any(|f| f == item) {
return false;
}
if m.ai.target.is_some() {
panda::got_bamboo(m);
}
let at = [m.position.x, m.position.y, m.position.z];
let mut dropped = None;
if m.is_baby() {
// ageUp((int) (-getAge() / 20 * 0.1F), true).
let seconds = ((-m.extra.age / 20) as f32 * 0.1) as i32;
horizon_client::mobs::ai::age_up(m, seconds, true);
} else if m.extra.age == 0 && m.extra.in_love <= 0 {
m.extra.in_love = 600;
m.extra.love_player = true;
} else {
if panda::is_sitting(m) || m.fluids.in_water {
return false;
}
panda::try_to_sit(m);
panda::start_eating(m);
// What it held falls, unless the player has infinite materials.
if let Some(held) = m.extra.main_hand.clone()
&& !creative
{
dropped = Some(ItemStack::new(&held, panda::held_count(m)));
}
panda::hold(m, item, 1, false);
}
self.consume_held();
if let Some(stack) = dropped {
self.spawn_item(stack, at, [0.0, 0.2, 0.0], 10);
}
true
}
/// `Panda.afterSneeze`: the grown pandas within 10 blocks of a cub jump.
pub(super) fn panda_sneezed(&mut self, id: u32) {
let Some(area) = self
.mobs
.iter()
.find(|m| m.id == id)
.map(|m| m.bounds().inflate(Vec3::new(10.0, 10.0, 10.0)))
else {
return;
};
for m in &mut self.mobs {
if m.species() == Species::Panda
&& !m.is_baby()
&& m.on_ground
&& !m.fluids.in_water
&& panda::can_perform_action(m, panda::scared(m))
&& area.intersects(m.bounds())
{
// LivingEntity.jumpFromGround.
m.needs_sync = true;
let power = f64::from(m.jump_power());
if power > 1.0e-5 {
m.velocity.y = power.max(m.velocity.y);
}
}
}
}
/// What a window draws of its pandas: the crumbs of a bite
/// (`addEatingParticles`) and the sneeze's particle.
pub(super) fn panda_effects(&mut self) {
let mut bites = Vec::new();
let mut sneezes = Vec::new();
for m in self
.mobs
.iter_mut()
.filter(|m| m.species() == Species::Panda)
{
let (crumbs, sneeze) = panda::effects(m);
if let (Some(crumbs), Some(item)) = (crumbs, m.extra.main_hand.clone()) {
bites.push((item, crumbs));
}
sneezes.extend(sneeze);
}
for (item, crumbs) in bites {
let options = self.item_particle(&ItemStack::new(&item, 1));
for (at, velocity) in crumbs {
self.particles.add(&options, at, velocity);
}
}
let options = ParticleOptions::simple(ParticleType::Sneeze);
for (at, velocity) in sneezes {
self.particles.add(&options, at, velocity);
}
}
}
+50 -3
View File
@@ -3466,6 +3466,7 @@ impl Game<'_> {
self.border.tick();
self.player_border();
self.tick_remote_entities(running);
self.tick_remote_guardian_eyes(running);
// ClientLevel.tickBlockEntities: chest lids, shulker boxes, bells,
// and the moving pistons a block event's client-side moveBlocks made
// (their progress advances between the server's block updates).
@@ -3769,6 +3770,43 @@ impl Game<'_> {
}
/// ClientLevel.tickEntities; a frozen level only ticks the players
/// (`TickRateManager.isEntityFrozen`).
/// `GuardianRenderer.getEntityToLookAt`: a guardian's eye follows its
/// synched attack target, else the camera.
fn tick_remote_guardian_eyes(&mut self, running: bool) {
use horizon_client::mobs::{Species, Who, guardian};
if !running {
return;
}
let camera = self.player.as_ref().map(|p| {
[
p.position.x,
p.position.y + f64::from(p.dimensions.eye_height),
p.position.z,
]
});
let looks: Vec<_> = self
.mobs
.iter()
.enumerate()
.filter(|(_, m)| {
matches!(m.species(), Species::Guardian | Species::ElderGuardian) && m.alive()
})
.filter_map(|(i, m)| {
let look = match m.ai.target.filter(|_| m.extra.flag) {
Some(Who::Mob(id)) => self
.mobs
.iter()
.find(|t| t.id == id)
.map(|t| [t.position.x, t.eye_y(), t.position.z]),
_ => camera,
};
look.map(|look| (i, look))
})
.collect();
for (i, look) in looks {
guardian::look_at(&mut self.mobs[i], look);
}
}
fn tick_remote_entities(&mut self, running: bool) {
if running {
self.tick_text_displays();
@@ -3777,16 +3815,18 @@ impl Game<'_> {
// server never sends the fluid contact, and the renderers read it (cod, salmon and
// tropical fish lie on their side without `in_water`, #1591).
// A frozen level ticks only the players, as below.
let fluids: Vec<_> = {
let (fluids, thundering): (Vec<_>, bool) = {
let w = self.mob_world_base(&[]);
let players = self.remote.as_ref().map(|r| &r.level_players);
self.mobs
let fluids = self
.mobs
.iter()
.map(|m| {
(running || players.is_some_and(|p| p.contains_key(&(m.id as i32))))
.then(|| w.fluids(m.bounds(), m.eye_y()))
})
.collect()
.collect();
(fluids, w.thundering)
};
for (m, fluids) in self.mobs.iter_mut().zip(fluids) {
if let Some(fluids) = fluids {
@@ -3868,6 +3908,10 @@ impl Game<'_> {
m.head_yaw += wrap_degrees(*yaw - m.head_yaw) / *steps as f32;
*steps -= 1;
}
// Fox.tick: the head tilt and crouch follow the synched flags.
if m.species() == horizon_client::mobs::Species::Fox {
horizon_client::mobs::fox::client_tick(m);
}
// Chicken.aiStep: the wings follow the server's on-ground flag.
if m.species() == horizon_client::mobs::Species::Chicken {
horizon_client::mobs::farm::chicken_flap(m);
@@ -3919,6 +3963,9 @@ impl Game<'_> {
}
// The animation states a server computes in its own `ai_step`.
horizon_client::mobs::sync::client_tick(m, game_time);
if m.species() == horizon_client::mobs::Species::Panda {
horizon_client::mobs::panda::client_tick(m, thundering);
}
}
for o in self.orbs.iter_mut().filter(|_| running) {
o.previous = o.position;
@@ -4,10 +4,12 @@
//! own simulation, which a client of a server doesn't run.
use super::Game;
use horizon_client::mobs::render::{PROFESSIONS, VILLAGER_TYPES};
use horizon_client::mobs::{Mob, Species, Who, bat, camel, sniffer, sync, tame};
use horizon_client::mobs::{
Mob, Species, Who, armadillo, bat, bee, camel, creaking, finalize, panda, sniffer, sync, tame,
};
use horizon_client::particles::{ParticleOptions, ParticleType};
use horizon_protocol::entity_data::{Particle, ParticleData, Value};
use horizon_protocol::registry::Builtin;
use horizon_protocol::registry::{Builtin, Dynamic};
/// The values `apply` takes from one SetEntityData packet, by vanilla's entry
/// indices (Entity 0..=7, LivingEntity 8..=14, Mob 15, then the subclasses).
@@ -72,10 +74,23 @@ fn apply(m: &mut Mob, values: &[(u8, i32, Value)], builtin: &Builtin, local_enti
Who::Mob(*target as u32)
});
}
// Panda: the counters, the two genes and DATA_ID_FLAGS (after
// AgeableMob's two entries).
(Species::Panda, 18, Value::Int(n)) => panda::set_unhappy(m, *n),
(Species::Panda, 19, Value::Int(n)) => panda::set_sneeze(m, *n),
(Species::Panda, 20, Value::Int(n)) => panda::set_eat(m, *n),
(Species::Panda, 21, Value::Byte(gene)) => m.extra.variant = (*gene).clamp(0, 6) as u8,
(Species::Panda, 22, Value::Byte(gene)) => m.extra.variant2 = (*gene).clamp(0, 6) as u8,
(Species::Panda, 23, Value::Byte(flags)) => panda::set_flags(m, *flags as u8),
// PolarBear.DATA_STANDING_ID: the client animates the rearing.
(Species::PolarBear, 18, Value::Bool(standing)) => m.extra.flag = *standing,
// Bat.DATA_ID_FLAGS: resting in bit 0.
(Species::Bat, 16, Value::Byte(flags)) => bat::set_resting(m, flags & 1 != 0),
// Fox.DATA_TYPE_ID and DATA_FLAGS_ID.
(Species::Fox, 18, Value::Int(variant)) => {
m.extra.variant = (*variant).clamp(0, 1) as u8
}
(Species::Fox, 19, Value::Byte(flags)) => m.extra.ints[0] = i32::from(*flags as u8),
// Sheep.DATA_WOOL_ID: the colour in the low nibble, 16 sheared.
(Species::Sheep, 18, Value::Byte(wool)) => {
m.extra.color = (*wool & 15) as u8;
@@ -113,6 +128,15 @@ fn apply(m: &mut Mob, values: &[(u8, i32, Value)], builtin: &Builtin, local_enti
(Species::Sniffer, 18, Value::Id(state)) => {
sniffer::set_state(m, (*state).clamp(0, 6) as u8);
}
// Armadillo.ARMADILLO_STATE (after AgeableMob's two): the shell and
// its animations follow it in `sync::client_tick`.
(Species::Armadillo, 18, Value::Id(state)) => {
armadillo::set_state(m, (*state).clamp(0, 3) as u8);
}
// Goat.DATA_HAS_LEFT_HORN and DATA_HAS_RIGHT_HORN: the model hides
// a horn the goat lost.
(Species::Goat, 19, Value::Bool(has)) => m.extra.lost_horns[0] = !*has,
(Species::Goat, 20, Value::Bool(has)) => m.extra.lost_horns[1] = !*has,
// Shulker.DATA_ATTACH_FACE_ID, DATA_PEEK_ID and DATA_COLOR_ID.
(Species::Shulker, 16, Value::Id(face)) => m.extra.variant3 = (*face).clamp(0, 5) as u8,
(Species::Shulker, 17, Value::Byte(peek)) => m.extra.counter = i32::from(*peek),
@@ -125,6 +149,66 @@ fn apply(m: &mut Mob, values: &[(u8, i32, Value)], builtin: &Builtin, local_enti
(Species::Camel | Species::CamelHusk, 20, Value::Long(tick)) => {
camel::set_synced_pose_tick(m, *tick);
}
// Vex.DATA_FLAGS_ID: charging in bit 0.
(Species::Vex, 16, Value::Byte(flags)) => m.extra.flag = flags & 1 != 0,
// Creaking.CAN_MOVE, IS_ACTIVE and IS_TEARING_DOWN.
(Species::Creaking, 16, Value::Bool(can_move)) => m.extra.flag2 = !*can_move,
(Species::Creaking, 17, Value::Bool(active)) => m.extra.flag = *active,
(Species::Creaking, 18, Value::Bool(true)) => creaking::set_tearing_down(m),
// Allay.DATA_DANCING and DATA_CAN_DUPLICATE (a cooldown running
// is any non-zero `egg_time`).
(Species::Allay, 16, Value::Bool(dancing)) => m.extra.flag = *dancing,
(Species::Allay, 17, Value::Bool(can)) => m.extra.egg_time = i32::from(!*can),
// Axolotl.DATA_PLAYING_DEAD (after DATA_VARIANT): the factors
// ease in `sync::client_tick`.
(Species::Axolotl, 19, Value::Bool(dead)) => m.extra.axolotl.playing_dead = *dead,
// CopperGolem.COPPER_GOLEM_STATE (after DATA_WEATHER_STATE).
(Species::CopperGolem, 17, Value::Id(state)) => {
m.extra.variant3 = (*state).clamp(0, 4) as u8;
}
// AbstractHorse's flags take 18. Horse.DATA_ID_TYPE_VARIANT: the
// colour in the low byte, the markings in the next.
(Species::Horse, 19, Value::Int(v)) => {
m.extra.variant = (v & 0xFF).clamp(0, 6) as u8;
m.extra.variant2 = ((v >> 8) & 0xFF).clamp(0, 4) as u8;
}
// AbstractChestedHorse.DATA_ID_CHEST, then Llama.DATA_STRENGTH_ID
// and DATA_VARIANT_ID.
(
Species::Donkey | Species::Mule | Species::Llama | Species::TraderLlama,
19,
Value::Bool(chested),
) => m.extra.chested = *chested,
(Species::Llama | Species::TraderLlama, 20, Value::Int(strength)) => {
m.extra.variant2 = (*strength).clamp(1, 5) as u8;
}
(Species::Llama | Species::TraderLlama, 21, Value::Int(variant)) => {
m.extra.variant = (*variant).clamp(0, 3) as u8;
}
// Axolotl.DATA_VARIANT (lucy 0 to blue 4) and Parrot.DATA_VARIANT_ID
// (after TamableAnimal's flags and owner).
(Species::Axolotl, 18, Value::Int(v)) => m.extra.variant = (*v).clamp(0, 4) as u8,
(Species::Parrot, 20, Value::Int(v)) => m.extra.variant = (*v).clamp(0, 4) as u8,
// Rabbit.DATA_TYPE_ID: the killer bunny's 99 has no model here.
(Species::Rabbit, 18, Value::Int(t)) if (0..=5).contains(t) => {
m.extra.variant = *t as u8;
}
// Bee.DATA_FLAGS_ID and DATA_ANGER_END_TIME (after Animal's).
(Species::Bee, 18, Value::Byte(flags)) => bee::set_synced(m, Some(*flags as u8), None),
(Species::Bee, 19, Value::Long(end)) => bee::set_synced(m, None, Some(*end)),
// AbstractFish.FROM_BUCKET takes 16: Salmon.DATA_TYPE (small 0
// to large 2), Pufferfish.PUFF_STATE and
// TropicalFish.DATA_ID_TYPE_VARIANT.
(Species::Salmon, 17, Value::Int(v)) => m.extra.variant = (*v).clamp(0, 2) as u8,
(Species::Pufferfish, 17, Value::Int(state)) => m.extra.size = (*state).clamp(0, 2),
(Species::TropicalFish, 17, Value::Int(v)) => tropical_fish(m, *v),
// Ghast.DATA_IS_CHARGING and WitherBoss.DATA_ID_INV.
(Species::Ghast, 16, Value::Bool(charging)) => m.extra.flag = *charging,
(Species::Wither, 19, Value::Int(ticks)) => m.extra.counter = (*ticks).max(0),
// CopperGolem.DATA_WEATHER_STATE: the oxidation the texture shows.
(Species::CopperGolem, 16, Value::Id(state)) => {
m.extra.variant = (*state).clamp(0, 3) as u8;
}
// Pillager.IS_CHARGING_CROSSBOW: the charge pose (Raider's
// IS_CELEBRATING takes 16).
(Species::Pillager, 17, Value::Bool(charging)) => m.extra.flag = *charging,
@@ -146,6 +230,56 @@ fn apply(m: &mut Mob, values: &[(u8, i32, Value)], builtin: &Builtin, local_enti
}
}
/// A tropical fish's packed variant: the pattern's id (the size, then the
/// index << 8), the base colour << 16 and the pattern colour << 24. Here the
/// pattern is 0 to 5 for the small ones and 6 to 11 for the large ones.
fn tropical_fish(m: &mut Mob, packed: i32) {
let (size, index) = (packed & 0xFF, (packed >> 8) & 0xFF);
// TropicalFish.Pattern.byId: an unknown id is a kob.
m.extra.variant = if size <= 1 && index < 6 {
(size * 6 + index) as u8
} else {
0
};
m.extra.variant2 = ((packed >> 16) & 0xFF) as u8 & 15;
m.extra.variant3 = ((packed >> 24) & 0xFF) as u8 & 15;
}
/// The values of one SetEntityData packet that name an entry of a
/// synchronized registry (the variants of the cat, wolf, frog, pig, cow,
/// chicken and zombie nautilus) or count game time (a wolf's anger).
/// `game_time` is the client level's.
fn apply_registry_data(
m: &mut Mob,
values: &[(u8, i32, Value)],
registries: &Dynamic,
game_time: i64,
) {
let species = m.species();
for (index, _, value) in values {
let registry = match (species, *index, value) {
(Species::Cat, 20, Value::Id(_)) => "minecraft:cat_variant",
(Species::Wolf, 23, Value::Id(_)) => "minecraft:wolf_variant",
(Species::Frog, 18, Value::Id(_)) => "minecraft:frog_variant",
(Species::Pig, 19, Value::Id(_)) => "minecraft:pig_variant",
(Species::Cow, 18, Value::Id(_)) => "minecraft:cow_variant",
(Species::Chicken, 18, Value::Id(_)) => "minecraft:chicken_variant",
(Species::ZombieNautilus, 21, Value::Id(_)) => "minecraft:zombie_nautilus_variant",
// Wolf.DATA_ANGER_END_TIME: the angry texture while it lies ahead.
(Species::Wolf, 22, Value::Long(end)) => {
m.extra.anger = (end - game_time).clamp(0, i64::from(i32::MAX)) as i32;
continue;
}
_ => continue,
};
if let Value::Id(id) = value
&& let Some(name) = registries.name(registry, *id)
{
m.extra.variant = finalize::variant_index(species, name);
}
}
}
/// The options of a synched particle list: an entity effect carries its ARGB
/// colour, the omens none.
fn swirls(list: &[Particle], builtin: &Builtin) -> Vec<ParticleOptions> {
@@ -200,6 +334,7 @@ impl Game<'_> {
};
if let Some(m) = self.mobs.iter_mut().find(|m| m.id == id as u32) {
apply(m, values, &r.builtin, r.entity_id);
apply_registry_data(m, values, &r.registries, self.world.game_time);
}
}
}
+33
View File
@@ -11,6 +11,7 @@ pub mod ambient;
pub mod anim;
pub mod animal_brain;
pub mod aquatic;
pub mod armadillo;
pub mod axolotl;
pub mod bat;
pub mod bee;
@@ -32,8 +33,11 @@ pub mod enderman;
pub mod equine;
pub mod farm;
pub mod finalize;
pub mod fox;
pub mod frog;
pub mod gear;
pub mod goals;
pub mod goat;
pub mod golem;
pub mod guardian;
pub mod happy_ghast;
@@ -48,6 +52,7 @@ pub mod monster_brain;
pub mod nautilus;
pub mod nav;
pub mod nether;
pub mod panda;
pub mod path;
pub mod phantom;
pub mod piglin;
@@ -798,6 +803,8 @@ pub struct Extra {
/// A goat's `IsScreamingGoat` and the horns it lost (left, right).
pub screaming: bool,
pub lost_horns: [bool; 2],
/// A goat's lowered head and ram behaviours.
pub goat: goat::State,
/// Attribute bases and permanent modifiers set by `finalizeSpawn`.
pub attributes: Option<Box<finalize::Attributes>>,
/// Brain memories `finalizeSpawn` sets: (memory, value, ticks to expiry).
@@ -813,6 +820,12 @@ pub struct Extra {
pub brain: Option<Box<monster_brain::Brain>>,
/// A bee's nectar, anger, flower and hive state.
pub bee: Option<Box<bee::Bee>>,
/// An armadillo's state and scare brain.
pub armadillo: armadillo::State,
/// A frog's brain memories and tongue target.
pub frog: frog::State,
/// A panda's flags, counters and amounts.
pub panda: Option<Box<panda::State>>,
}
#[derive(Clone, Debug)]
pub struct Mob {
@@ -1059,6 +1072,13 @@ pub enum MobEvent {
amount: f32,
source: Damage,
},
/// `LivingEntity.knockback(power, x, z)`: pushed away from the direction.
Knockback {
target: Who,
power: f64,
x: f64,
z: f64,
},
/// Set another mob's attack target (HurtByTargetGoal alerts).
SetTarget {
mob: u32,
@@ -1308,6 +1328,10 @@ pub enum MobEvent {
center: [f64; 3],
spread: [f64; 3],
},
/// `Panda.afterSneeze`: the grown pandas within 10 blocks start.
PandaSneezed {
mob: u32,
},
}
/// How a spawned-by-kind mob should be set up.
#[derive(Clone, Debug, Default)]
@@ -1345,6 +1369,8 @@ pub struct Seen {
pub target: Option<Who>,
pub health: f32,
pub variant: u8,
/// A slime's or magma cube's size.
pub size: i32,
/// Dye colour (sheep).
pub color: u8,
/// A villager's claimed job site and bed.
@@ -1448,6 +1474,7 @@ impl Mob {
target: m.ai.target,
health: m.health,
variant: m.extra.variant,
size: m.extra.size,
color: m.extra.color,
job: villager.then_some(m.extra.home).flatten(),
bed: villager.then_some(m.extra.bed).flatten(),
@@ -1632,6 +1659,9 @@ pub struct PlayerView {
pub last_hurt: Option<(Who, i64)>,
pub velocity: Vec3,
pub yaw: f32,
/// `Player.isSprinting` and `isPassenger` (what scares an armadillo).
pub sprinting: bool,
pub riding: bool,
/// The Hero of the Village effect.
pub hero: bool,
/// The active effects, without namespace (`slowness`, `poison`...).
@@ -1925,6 +1955,9 @@ impl Mob {
return false;
}
let mut amount = if reflected { 1000.0 } else { amount };
if self.species() == Species::Armadillo {
amount = armadillo::reduce_damage(self, amount);
}
if source.kind == DamageKind::FallingBlock
&& !species::body_slot(self.species())
&& self.extra.head.is_some()
+59 -15
View File
@@ -106,6 +106,7 @@ pub enum Family {
Strider,
Bat,
Bee,
Panda,
PolarBear,
}
pub fn family(s: Species) -> Family {
@@ -168,6 +169,7 @@ pub fn family(s: Species) -> Family {
S::Strider => Family::Strider,
S::Bat => Family::Bat,
S::Bee => Family::Bee,
S::Panda => Family::Panda,
S::PolarBear => Family::PolarBear,
_ => Family::Generic,
}
@@ -180,6 +182,9 @@ pub fn register(m: &mut Mob) {
Family::Undead => undead::register(m),
Family::Hostile => hostile::register(m),
Family::Aquatic => aquatic::register(m),
Family::Generic if m.species() == Species::Armadillo => super::armadillo::register(m),
Family::Generic if m.species() == Species::Frog => super::frog::register(m),
Family::Generic if m.species() == Species::Fox => super::fox::register(m),
Family::Generic => {
generic(m);
if m.species() == Species::Rabbit {
@@ -200,19 +205,8 @@ pub fn register(m: &mut Mob) {
if m.species() == Species::Turtle {
super::turtle::register(m);
}
if m.species() == Species::Frog {
// `Frog()`: SmoothSwimmingMoveControl(85, 10, 0.02, 0.1, true).
// The frog's movement speed is 1.0, so the controller's 0.1
// outside-water speed is what keeps a walking frog at
// vanilla's pace; the plain ground controller walks ten
// times too fast (#2213).
m.ai.mov.kind = super::control::MoveKind::SmoothSwimming {
max_turn_x: 85.0,
max_turn_y: 10.0,
in_water: 0.02,
outside: 0.1,
gravity: true,
};
if m.species() == Species::Goat {
super::goat::register(m);
}
}
Family::Inert => {}
@@ -255,6 +249,7 @@ pub fn register(m: &mut Mob) {
Family::Strider => super::strider::register(m),
Family::Bat => super::bat::register(m),
Family::Bee => super::bee::register(m),
Family::Panda => super::panda::register(m),
Family::PolarBear => super::polar_bear::register(m),
}
// FloatGoal's constructor lets the navigation float, as do the
@@ -455,7 +450,10 @@ pub fn tick(
}
// Mob.tickHeadTurn -> BodyRotationControl.clientTick (a frozen
// creaking's waits).
if m.alive() && (!super::creaking::is_creaking(m) || super::creaking::can_move(m)) {
if m.alive()
&& (!super::creaking::is_creaking(m) || super::creaking::can_move(m))
&& !super::armadillo::scared(m)
{
m.body_rotation_tick();
}
events.append(&mut m.outbox);
@@ -477,6 +475,9 @@ pub fn tick(
if m.species() == Species::Bee {
super::bee::tick(m, level_random);
}
if m.species() == Species::Panda {
super::panda::tick(m, w, events);
}
true
}
/// LivingEntity.updateSwingTime (6-tick swing duration).
@@ -782,6 +783,9 @@ fn ai_step(m: &mut Mob, w: &MobWorld, level_random: &mut LegacyRandom, events: &
.is_some_and(|t| t.position.distance_squared(m.position) > 121.0);
super::witch::ai_step(m, events, far);
}
if m.species() == Species::Fox {
super::fox::pre_ai_step(m);
}
// AbstractHorse.aiStep: the tail swishes now and then (moveTail).
if super::equine::is_equine(m.species()) {
super::equine::tail_tick(m);
@@ -1108,7 +1112,11 @@ fn armadillo_server_ai_step(m: &mut Mob, mob_drops: bool, events: &mut Vec<MobEv
fn custom_server_ai_step(m: &mut Mob, cx: &mut Cx) {
match m.species() {
Species::Rabbit => super::rabbit::server_ai_step(m),
Species::Armadillo => armadillo_server_ai_step(m, cx.w.mob_drops, cx.events),
// Armadillo.customServerAiStep: the brain and its activity, then the scute.
Species::Armadillo => {
super::armadillo::server_ai_step(m, cx);
armadillo_server_ai_step(m, cx.w.mob_drops, cx.events);
}
Species::Sheep => {}
Species::Zombie
| Species::Husk
@@ -1129,6 +1137,7 @@ fn custom_server_ai_step(m: &mut Mob, cx: &mut Cx) {
Species::Evoker if m.extra.counter > 0 => m.extra.counter -= 1,
Species::Bat => super::bat::server_ai_step(m, cx.w, cx.events),
Species::Bee => super::bee::server_ai_step(m, cx),
Species::Frog => super::frog::server_ai_step(m, cx.w),
_ => {}
}
}
@@ -1182,6 +1191,10 @@ fn species_ai_step(
}
Species::Nautilus | Species::ZombieNautilus => super::nautilus::ai_step(m, w, events),
Species::Chicken => farm::chicken_ai_step(m, w, events),
Species::Armadillo => super::armadillo::tick(m),
Species::Frog => super::frog::tick(m),
Species::Goat => super::goat::tick(m),
Species::Fox => super::fox::ai_step(m, w, events),
Species::Turtle => super::turtle::ai_step(m, w, events),
Species::Sheep if m.extra.anim_ticks > 0 => {
m.extra.anim_ticks -= 1;
@@ -1300,6 +1313,18 @@ pub fn voice_sounds(m: &Mob) -> [&'static str; 3] {
match m.species() {
Species::Nautilus | Species::ZombieNautilus => super::nautilus::sounds(m),
Species::SulfurCube => super::sulfur_cube::sounds(m),
Species::Armadillo => super::armadillo::sounds(m),
// Panda.getAmbientSound.
Species::Panda => {
let ambient = if super::panda::is_aggressive(m) {
"entity.panda.aggressive_ambient"
} else if super::panda::is_worried(m) {
"entity.panda.worried_ambient"
} else {
m.kind.sounds[0]
};
[ambient, m.kind.sounds[1], m.kind.sounds[2]]
}
// The sound variant's baby_sounds (classic sets, like the rest here).
Species::Pig if m.is_baby() => [
"entity.baby_pig.ambient",
@@ -1322,6 +1347,10 @@ pub fn feed_animal(
m: &mut Mob,
level: &mut impl RandomSource,
) -> Option<Option<(&'static str, f32, f32)>> {
// Armadillo.mobInteract: a ball takes no food.
if super::armadillo::scared(m) {
return None;
}
if m.extra.age == 0 && m.extra.in_love <= 0 {
m.extra.in_love = 600;
m.extra.love_player = true;
@@ -1498,6 +1527,11 @@ pub fn accepts_damage(m: &mut Mob, source: &Damage, _amount: f32) -> bool {
super::bee::hurt(m);
true
}
// Panda.hurtServer: up first, whatever the damage.
Species::Panda => {
super::panda::hurt(m);
true
}
Species::Enderman => super::enderman::on_damage(m, source),
Species::WitherSkeleton | Species::Blaze | Species::MagmaCube | Species::Strider
if source.is_fire() =>
@@ -1512,6 +1546,7 @@ pub fn after_hurt(m: &mut Mob, source: &Damage) {
m.ai.no_action_time = 0;
match m.species() {
Species::Enderman => super::enderman::after_hurt(m, source),
Species::Armadillo => super::armadillo::after_hurt(m, source),
Species::Shulker => super::shulker::after_hurt(m),
Species::Warden => super::warden::hurt(m, source),
Species::Wolf => super::tame::after_hurt(m, source),
@@ -1558,6 +1593,9 @@ pub fn do_hurt_target(m: &mut Mob, who: Who, cx: &mut Cx) {
if m.species() == Species::Ravager {
super::ravager::on_attack(m, cx.events);
}
if m.species() == Species::Panda {
super::panda::bit(m);
}
if super::creaking::is_creaking(m) {
super::creaking::on_attack(m, cx.events);
}
@@ -1667,6 +1705,12 @@ pub fn do_hurt_target(m: &mut Mob, who: Who, cx: &mut Cx) {
if super::creaking::is_creaking(m) {
m.voice("entity.creaking.attack", cx.events);
}
// Panda.playAttackSound: playSound(PANDA_BITE, 1.0, 1.0).
if m.species() == Species::Panda && !m.silent {
let at = [m.position.x, m.position.y, m.position.z];
cx.events
.push(MobEvent::Sound("entity.panda.bite", at, 1.0, 1.0));
}
// Axolotl.playAttackSound: playSound(AXOLOTL_ATTACK, 1.0, 1.0).
if m.species() == Species::Axolotl {
let at = [m.position.x, m.position.y, m.position.z];
+5
View File
@@ -133,6 +133,11 @@ pub fn ai_step(m: &mut Mob) {
if m.extra.egg_time > 0 {
m.extra.egg_time -= 1;
}
animate(m);
}
/// `Allay.tick`'s client half: the hold and spin clocks (the server's and a
/// client's).
pub fn animate(m: &mut Mob) {
let holding = m.extra.main_hand.is_some();
let v = &mut m.anim.v;
v[HOLD] = (v[HOLD] + if holding { 1.0 } else { -1.0 }).clamp(0.0, 5.0);
+407
View File
@@ -0,0 +1,407 @@
//! Armadillos (`Armadillo`, `ArmadilloAi`): a player who sprints or rides
//! nearby, an undead mob or the last attacker scares one into a ball (IDLE,
//! ROLLING, SCARED, UNROLLING); it peeks out now and then, and a ball takes
//! less damage. The brain's `ARMADILLO_SCARE_DETECTED` sensor, its
//! `DANGER_DETECTED_RECENTLY` memory and the `ArmadilloBallUp` behaviour are
//! kept in `State`; the walking behaviours are the goals of `register`.
//! AnimState states: roll out, roll up, peek.
use super::{
Damage, DamageKind, Mob, MobEvent, MobWorld, Species, Who,
goals::{
Cx, Float, Goal, LOOK, LookAtPlayer, MOVE, Panic, RandomLookAround, Stroll, goal_clone,
},
};
use horizon_core::{geometry::Vec3, random::RandomSource};
pub const IDLE: u8 = 0;
pub const ROLLING: u8 = 1;
pub const SCARED: u8 = 2;
pub const UNROLLING: u8 = 3;
const ROLL_OUT: usize = 0;
const ROLL_UP: usize = 1;
const PEEK: usize = 2;
/// `ArmadilloState.animationDuration`.
fn animation_duration(state: u8) -> i32 {
match state {
ROLLING => 10,
SCARED => 50,
UNROLLING => 30,
_ => 0,
}
}
/// `Armadillo.ARMADILLO_STATE` and the brain's memory and behaviour.
#[derive(Clone, Debug, Default)]
pub struct State {
pub state: u8,
/// `inStateTicks`.
pub in_state_ticks: i32,
/// `DANGER_DETECTED_RECENTLY`: ticks to live, 0 when absent.
danger: i32,
/// Ticks to the sensor's next scan.
sensor: i32,
/// `ArmadilloBallUp`: running, its peek timer and whether it saw the
/// danger around last tick.
ball_up: bool,
next_peek: i32,
danger_was_around: bool,
/// Whether the PANIC activity was the active one after the last tick.
panic_activity: bool,
/// `peekReceivedClient`.
peek_received: bool,
}
pub fn register(m: &mut Mob) {
let mut panic = Panic::new(2.0);
panic.environmental = true;
let g = &mut m.ai.goals;
g.add(0, Float);
g.add(1, ArmadilloPanic(panic));
g.add(2, Rolled);
g.add(5, Stroll::avoiding_water(1.0));
g.add(6, LookAtPlayer::new(6.0));
g.add(7, RandomLookAround::default());
}
/// `Armadillo.isScared`.
pub fn scared(m: &Mob) -> bool {
m.extra.armadillo.state != IDLE
}
/// `ArmadilloState.shouldHideInShell`: the model shows its shell.
pub fn hiding_in_shell(m: &Mob) -> bool {
let a = &m.extra.armadillo;
match a.state {
ROLLING => a.in_state_ticks > 5,
SCARED => true,
UNROLLING => a.in_state_ticks < 26,
_ => false,
}
}
/// `Armadillo.getState`, as it is synched.
pub fn state(m: &Mob) -> u8 {
m.extra.armadillo.state
}
/// `Armadillo.switchToState` and `onSyncedDataUpdated(ARMADILLO_STATE)`.
pub fn set_state(m: &mut Mob, state: u8) {
let a = &mut m.extra.armadillo;
if a.state != state {
a.state = state;
a.in_state_ticks = 0;
}
}
/// `Armadillo.canStayRolledUp`.
fn can_stay_rolled_up(m: &Mob) -> bool {
!m.ai.panicking
&& !m.fluids.in_water
&& !m.fluids.in_lava
&& m.extra.leash.is_none()
&& m.extra.mount.is_none()
&& m.extra.rider.is_none()
}
/// `Entity.playSound(sound)`.
fn play(m: &Mob, events: &mut Vec<MobEvent>, sound: &'static str) {
if !m.silent {
let at = [m.position.x, m.position.y, m.position.z];
events.push(MobEvent::Sound(sound, at, 1.0, 1.0));
}
}
/// `Armadillo.rollUp`.
fn roll_up(m: &mut Mob, events: &mut Vec<MobEvent>) {
if !scared(m) {
super::creaking::stop_in_place(m);
m.extra.in_love = 0;
if !m.silent {
m.voice("entity.armadillo.roll", events);
}
set_state(m, ROLLING);
}
}
/// `Armadillo.rollOut`.
fn roll_out(m: &mut Mob, events: &mut Vec<MobEvent>) {
if scared(m) {
if !m.silent {
m.voice("entity.armadillo.unroll_finish", events);
}
set_state(m, IDLE);
}
}
/// `Armadillo.hurtServer`: a ball takes half of the damage past the first
/// point.
pub fn reduce_damage(m: &Mob, amount: f32) -> f32 {
if scared(m) {
((amount - 1.0) / 2.0).max(0.0)
} else {
amount
}
}
/// `Armadillo.actuallyHurt`: a hit by a living thing curls it up (and keeps
/// the danger in mind for 80 ticks), a hit by fire and the like unrolls it.
pub fn after_hurt(m: &mut Mob, source: &Damage) {
if m.no_ai || m.health <= 0.0 {
return;
}
let mut events = Vec::new();
if source.attacker.is_some() {
m.extra.armadillo.danger = 80;
if can_stay_rolled_up(m) {
roll_up(m, &mut events);
}
} else if environmental(source.kind) {
roll_out(m, &mut events);
}
m.outbox.append(&mut events);
}
/// `DamageTypeTags.PANIC_ENVIRONMENTAL_CAUSES`.
pub fn environmental(kind: DamageKind) -> bool {
matches!(
kind,
DamageKind::Cactus | DamageKind::OnFire | DamageKind::InFire | DamageKind::Lava
)
}
/// The hurt sound: a ball's is muffled, and it has no ambient sound
/// (`getAmbientSound`, `getHurtSound`); empty is none.
pub fn sounds(m: &Mob) -> [&'static str; 3] {
let [ambient, hurt, death] = m.kind.sounds;
if scared(m) {
["", "entity.armadillo.hurt_reduced", death]
} else {
[ambient, hurt, death]
}
}
/// `EntityTypeTags.UNDEAD`.
fn undead(s: Species) -> bool {
matches!(
s,
Species::Skeleton
| Species::Stray
| Species::WitherSkeleton
| Species::SkeletonHorse
| Species::Bogged
| Species::Parched
| Species::ZombieHorse
| Species::CamelHusk
| Species::Zombie
| Species::ZombieVillager
| Species::ZombifiedPiglin
| Species::Zoglin
| Species::Drowned
| Species::Husk
| Species::ZombieNautilus
| Species::Wither
| Species::Phantom
)
}
/// `Armadillo.isScaredBy` for the nearby living entities: within 7 blocks
/// sideways and 2 up and down, an undead mob, the last attacker, or a
/// player who sprints or rides.
fn scared_by_someone(m: &Mob, w: &MobWorld) -> bool {
let area = m.bounds().inflate(Vec3::new(7.0, 2.0, 7.0));
let attacker =
m.ai.last_hurt_by
.filter(|_| m.age as i32 - m.ai.last_hurt_by_time <= 100);
if let Some(p) = &w.player
&& p.alive
&& !p.spectator
&& p.bounds.intersects(area)
&& (attacker == Some(Who::Player) || p.sprinting || p.riding)
{
return true;
}
w.others_near(area).any(|s| {
s.id != m.id
&& s.alive
&& s.bounds.intersects(area)
&& (undead(s.species) || attacker == Some(Who::Mob(s.id)))
})
}
/// `Armadillo.customServerAiStep`'s brain: memories expire, the scare
/// sensor scans every fifth tick, the core's rolling out, then the
/// `ArmadilloBallUp` behaviour of the PANIC activity.
pub fn server_ai_step(m: &mut Mob, cx: &mut Cx) {
let events = &mut *cx.events;
let a = &mut m.extra.armadillo;
if a.danger > 0 {
a.danger -= 1;
}
a.sensor -= 1;
if a.sensor <= 0 {
a.sensor = 5;
if !can_stay_rolled_up(m) {
m.extra.armadillo.danger = 0;
} else if scared_by_someone(m, cx.w) {
m.extra.armadillo.danger = 80;
}
}
// ARMADILLO_ROLLING_OUT: no danger left in mind.
if m.extra.armadillo.danger == 0 && scared(m) {
roll_out(m, events);
}
if m.extra.armadillo.panic_activity && !m.extra.armadillo.ball_up && m.on_ground && !liquid(m) {
// ArmadilloBallUp.start.
m.extra.armadillo.ball_up = true;
roll_up(m, events);
}
if m.extra.armadillo.ball_up {
ball_up_tick(m, events);
}
let a = &mut m.extra.armadillo;
a.panic_activity = a.danger > 0 && !m.ai.panicking;
}
fn liquid(m: &Mob) -> bool {
m.fluids.in_water || m.fluids.in_lava
}
/// `ArmadilloBallUp.tickOrStop`.
fn ball_up_tick(m: &mut Mob, events: &mut Vec<MobEvent>) {
if !scared(m) {
// canStillUse is false: stop.
m.extra.armadillo.ball_up = false;
if !can_stay_rolled_up(m) {
roll_out(m, events);
}
return;
}
let a = &mut m.extra.armadillo;
if a.next_peek > 0 {
a.next_peek -= 1;
}
if a.state == ROLLING && a.in_state_ticks > animation_duration(ROLLING) {
set_state(m, SCARED);
if m.on_ground {
play(m, events, "entity.armadillo.land");
}
return;
}
let danger = m.extra.armadillo.danger;
let around = danger > 75;
if around != m.extra.armadillo.danger_was_around {
m.extra.armadillo.next_peek = next_peek_timer(m);
}
m.extra.armadillo.danger_was_around = around;
let state = m.extra.armadillo.state;
if state == SCARED {
if m.extra.armadillo.next_peek == 0 && m.on_ground && around {
peek(m, events);
m.extra.armadillo.next_peek = next_peek_timer(m);
}
if danger < animation_duration(UNROLLING) {
play(m, events, "entity.armadillo.unroll_start");
set_state(m, UNROLLING);
}
} else if state == UNROLLING && danger > animation_duration(UNROLLING) {
set_state(m, SCARED);
}
}
/// `ArmadilloBallUp.pickNextPeekTimer`.
fn next_peek_timer(m: &mut Mob) -> i32 {
animation_duration(SCARED) + m.random.next_int_between_inclusive(100, 400).unwrap_or(100)
}
/// `level.broadcastEntityEvent(body, 64)`; the sound is the client's
/// `playLocalSound`, which the server plays for everyone here.
fn peek(m: &mut Mob, events: &mut Vec<MobEvent>) {
entity_event(m, 64);
let at = [m.position.x, m.position.y, m.position.z];
events.push(MobEvent::EntityEvent {
mob: m.id,
event: 64,
at,
});
events.push(MobEvent::Sound("entity.armadillo.peek", at, 1.0, 1.0));
}
/// `Armadillo.handleEntityEvent`: 64 is a peek.
pub fn entity_event(m: &mut Mob, event: u8) {
if event == 64 {
m.extra.armadillo.peek_received = true;
}
}
/// `Armadillo.tick`: the animation states on a client, the ball's head held
/// to its body, and `inStateTicks`. Server and client both run it.
pub fn tick(m: &mut Mob) {
animate(m);
if scared(m) {
// clampHeadRotationToBody with getMaxHeadYRot 0.
m.head_yaw += super::living::wrap(m.body_yaw - m.head_yaw);
}
m.extra.armadillo.in_state_ticks += 1;
}
/// `Armadillo.setupAnimationStates`.
fn animate(m: &mut Mob) {
let age = m.age as f32;
let a = &mut m.extra.armadillo;
let s = &mut m.anim.states;
match a.state {
ROLLING => {
s[ROLL_OUT].stop();
s[ROLL_UP].start_if_stopped(age);
s[PEEK].stop();
}
SCARED => {
s[ROLL_OUT].stop();
s[ROLL_UP].stop();
if a.peek_received {
s[PEEK].stop();
a.peek_received = false;
}
if a.in_state_ticks == 0 {
s[PEEK].start(age);
s[PEEK].fast_forward(animation_duration(SCARED));
} else {
s[PEEK].start_if_stopped(age);
}
}
UNROLLING => {
s[ROLL_OUT].start_if_stopped(age);
s[ROLL_UP].stop();
s[PEEK].stop();
}
_ => {
s[ROLL_OUT].stop();
s[ROLL_UP].stop();
s[PEEK].stop();
}
}
}
/// `ArmadilloAi.ArmadilloPanic`: it unrolls, then runs.
#[derive(Clone, Debug)]
struct ArmadilloPanic(Panic);
impl Goal for ArmadilloPanic {
goal_clone!();
fn flags(&self) -> u8 {
self.0.flags()
}
fn name(&self) -> &'static str {
"Panic"
}
fn can_use(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
self.0.can_use(m, cx)
}
fn can_continue(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
self.0.can_continue(m, cx)
}
fn start(&mut self, m: &mut Mob, cx: &mut Cx) {
roll_out(m, cx.events);
self.0.start(m, cx);
}
fn stop(&mut self, m: &mut Mob, cx: &mut Cx) {
self.0.stop(m, cx);
}
}
/// The behaviours that do not run while the armadillo is a ball: its
/// `MoveToTargetSink` and the idle activity's walking and looking.
#[derive(Clone, Debug)]
struct Rolled;
impl Goal for Rolled {
goal_clone!();
fn flags(&self) -> u8 {
MOVE | LOOK
}
fn can_use(&mut self, m: &mut Mob, _cx: &mut Cx) -> bool {
scared(m)
}
}
+11
View File
@@ -103,6 +103,17 @@ pub fn has_stung(m: &Mob) -> bool {
pub fn rolling(m: &Mob) -> bool {
flag(m, FLAG_ROLL)
}
/// A client's bee from the server's `DATA_FLAGS_ID` and `DATA_ANGER_END_TIME`
/// (the texture and the roll read them).
pub fn set_synced(m: &mut Mob, flags: Option<u8>, anger_end: Option<i64>) {
let b = st(m);
if let Some(flags) = flags {
b.flags = flags;
}
if let Some(anger_end) = anger_end {
b.anger_end = anger_end;
}
}
/// `Bee.setHasNectar`: nectar resets the time without it.
pub fn set_has_nectar(m: &mut Mob, value: bool) {
if value {
+7 -7
View File
@@ -59,11 +59,11 @@ pub struct Goat {
pub ramming_x_head_rot: f32,
}
impl Goat {
pub fn from_mob(_m: &Mob) -> Self {
pub fn from_mob(m: &Mob) -> Self {
Self {
left_horn: true,
right_horn: true,
ramming_x_head_rot: 0.0,
left_horn: !m.extra.lost_horns[0],
right_horn: !m.extra.lost_horns[1],
ramming_x_head_rot: super::goat::ramming_x_head_rot(m),
}
}
}
@@ -960,7 +960,7 @@ pub fn anim(m: &Mob, s: &State, p: &mut Poses, a: &Animations) -> bool {
match m.species() {
Species::Goat => goat(p, s, &Goat::from_mob(m)),
Species::PolarBear => polar_bear(p, s, super::polar_bear::standing_scale(m, s.partial)),
Species::Panda => panda(p, s, &Panda::default()),
Species::Panda => panda(p, s, &super::panda::pose(m, s.partial)),
Species::Llama | Species::TraderLlama => {
llama(p, s, m.extra.chested && !s.baby);
}
@@ -1008,7 +1008,7 @@ pub fn anim(m: &Mob, s: &State, p: &mut Poses, a: &Animations) -> bool {
p,
s,
&Armadillo {
hiding_in_shell: false,
hiding_in_shell: super::armadillo::hiding_in_shell(m),
roll_out: st[0],
roll_up: st[1],
peek: st[2],
@@ -1029,7 +1029,7 @@ pub fn anim(m: &Mob, s: &State, p: &mut Poses, a: &Animations) -> bool {
a,
),
Species::Rabbit => rabbit(p, s, &st[0], &st[1], a),
Species::Fox => fox(p, s, &Fox::default(), a),
Species::Fox => fox(p, s, &super::fox::pose(m, s.partial), a),
_ => return false,
}
true
+17 -1
View File
@@ -146,6 +146,16 @@ impl Mob {
if self.species() == super::Species::Rabbit {
return super::rabbit::move_control(self, w);
}
// Fox.FoxMoveControl.tick: still while asleep, sitting or faceplanted.
if self.species() == super::Species::Fox && !super::fox::can_move(self) {
return;
}
// Panda.PandaMoveControl: only while it can act.
if self.species() == super::Species::Panda
&& !super::panda::can_perform_action(self, w.thundering)
{
return;
}
if self.species() == super::Species::Turtle {
return super::turtle::move_control(self);
}
@@ -328,6 +338,11 @@ impl Mob {
/// LookControl.tick (and SmoothSwimmingLookControl).
pub fn look_control_tick(&mut self) {
let look = self.ai.look.clone();
let fox = self.species() == super::Species::Fox;
// Fox.FoxLookControl: no turning in sleep.
if fox && super::fox::flag(self, super::fox::SLEEPING) {
return;
}
if look.kind == LookKind::HappyGhast {
return super::happy_ghast::look_control(self);
}
@@ -374,7 +389,8 @@ impl Mob {
// clampHeadRotationToBody does nothing for a shulker.
}
LookKind::Default | LookKind::Fixed => {
if look.kind == LookKind::Default {
// FoxLookControl.resetXRotOnTick keeps a stalking or pouncing pitch.
if look.kind == LookKind::Default && (!fox || super::fox::resets_x_rot(self)) {
self.head_pitch = 0.0;
}
if look.cooldown > 0 {
+12 -2
View File
@@ -70,6 +70,16 @@ pub fn sound(m: &Mob, what: &str) -> &'static str {
}
/// `CopperGolem.setupAnimationStates` (client) and the sound of the head spin.
pub fn ai_step(m: &mut Mob, _w: &MobWorld, events: &mut Vec<MobEvent>) {
if animate(m) {
let at = [m.position.x, m.position.y, m.position.z];
events.push(MobEvent::LocalSound(sound(m, "spin"), at, 1.0, 1.0));
}
}
/// `CopperGolem.setupAnimationStates`, for the server and a client alike;
/// returns whether the head spin sound plays now (a client's sound is the
/// server's `LocalSound`).
pub fn animate(m: &mut Mob) -> bool {
let mut spin = false;
let age = m.age as f32;
let state = m.extra.variant3;
let st = &mut m.anim.states;
@@ -93,8 +103,7 @@ pub fn ai_step(m: &mut Mob, _w: &MobWorld, events: &mut Vec<MobEvent>) {
m.extra.ints[0] = tick + 200 + r.next_int_bound(40).unwrap_or(0);
}
if tick == m.extra.ints[0] + 10 {
let at = [m.position.x, m.position.y, m.position.z];
events.push(MobEvent::LocalSound(sound(m, "spin"), at, 1.0, 1.0));
spin = true;
m.extra.ints[0] = 0;
}
}
@@ -106,6 +115,7 @@ pub fn ai_step(m: &mut Mob, _w: &MobWorld, events: &mut Vec<MobEvent>) {
}
_ => {}
}
spin
}
/// `CopperGolem.updateWeathering` on the level random: the first tick sets
/// the next weathering tick, which advances the weather state; once
+49 -21
View File
@@ -152,8 +152,26 @@ pub fn sway(m: &mut Mob, events: &mut Vec<MobEvent>) {
}
/// `Creaking.doHurtTarget`: the attack animation (the sound is the
/// `playAttackSound` that ends `Mob.doHurtTarget`).
pub fn on_attack(m: &mut Mob, _events: &mut Vec<MobEvent>) {
pub fn on_attack(m: &mut Mob, events: &mut Vec<MobEvent>) {
m.extra.ints[ATTACKING] = ATTACK_TICKS;
broadcast(m, 4, events);
}
/// `Level.broadcastEntityEvent` for a creaking (guests run `entity_event`).
fn broadcast(m: &Mob, event: i8, events: &mut Vec<MobEvent>) {
events.push(MobEvent::EntityEvent {
mob: m.id,
event,
at: [m.position.x, m.position.y, m.position.z],
});
}
/// `Creaking.handleEntityEvent` on a client: 66 the invulnerability flash,
/// 4 the attack swing (the sounds are the server's).
pub fn entity_event(m: &mut Mob, event: u8) {
match event {
66 => m.extra.ints[INVULNERABLE] = INVULNERABILITY_TICKS,
4 => m.extra.ints[ATTACKING] = ATTACK_TICKS,
_ => {}
}
}
/// The heart this creaking is bound to protects it (`isProtector`); a link
/// read from a save is trusted until the heart resolves it.
@@ -176,15 +194,12 @@ pub fn update(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
if heart_bound(m) && m.alive() && !protected(m, w) {
m.health = 0.0;
}
for slot in [INVULNERABLE, ATTACKING] {
if m.extra.ints[slot] > 0 {
m.extra.ints[slot] -= 1;
}
// hurtServer set the invulnerability clock since the last tick: entity
// event 66.
if m.extra.ints[INVULNERABLE] == INVULNERABILITY_TICKS {
broadcast(m, 66, events);
}
let age = m.age as f32;
m.anim.states[0].animate_when(m.extra.ints[ATTACKING] > 0, age);
m.anim.states[1].animate_when(m.extra.ints[INVULNERABLE] > 0, age);
m.anim.states[2].animate_when(tearing_down(m), age);
animate(m);
if m.extra.counter & HURT != 0 {
// hurtServer: sway, and the heart reacts to a player's hit.
if protected(m, w) {
@@ -200,18 +215,6 @@ pub fn update(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
}
m.extra.counter &= !(HURT | HURT_BY_PLAYER);
}
// checkEyeBlink: the eyes of a creaking falling apart flicker.
if m.death_time > m.extra.ints[NEXT_FLICKER] {
let glowing = m.extra.ints[EYES] != 0;
let (lo, hi) = if glowing {
(2, 8)
} else {
(m.death_time / 4, m.death_time / 2)
};
let span = (hi - lo + 1).max(1);
m.extra.ints[NEXT_FLICKER] = m.death_time + lo + m.random.next_int_bound(span).unwrap_or(0);
m.extra.ints[EYES] = i32::from(!glowing);
}
if !m.alive() {
return;
}
@@ -226,6 +229,31 @@ pub fn update(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
m.extra.flag2 = !now;
}
}
/// The clocks and keyframe states `Creaking.aiStep`, `setupAnimationStates`
/// and `checkEyeBlink` run (the server's and a client's).
pub fn animate(m: &mut Mob) {
for slot in [INVULNERABLE, ATTACKING] {
if m.extra.ints[slot] > 0 {
m.extra.ints[slot] -= 1;
}
}
let age = m.age as f32;
m.anim.states[0].animate_when(m.extra.ints[ATTACKING] > 0, age);
m.anim.states[1].animate_when(m.extra.ints[INVULNERABLE] > 0, age);
m.anim.states[2].animate_when(tearing_down(m), age);
// checkEyeBlink: the eyes of a creaking falling apart flicker.
if m.death_time > m.extra.ints[NEXT_FLICKER] {
let glowing = m.extra.ints[EYES] != 0;
let (lo, hi) = if glowing {
(2, 8)
} else {
(m.death_time / 4, m.death_time / 2)
};
let span = (hi - lo + 1).max(1);
m.extra.ints[NEXT_FLICKER] = m.death_time + lo + m.random.next_int_bound(span).unwrap_or(0);
m.extra.ints[EYES] = i32::from(!glowing);
}
}
/// `Mob.stopInPlace`.
pub fn stop_in_place(m: &mut Mob) {
m.nav_stop();
+7 -1
View File
@@ -244,7 +244,13 @@ pub fn slot_item(m: &Mob, slot: &str) -> Option<LootStack> {
}
_ => (None, None),
};
let mut stack = loot_stack(item?, 1);
// A panda keeps the whole stack it picked up in its mouth.
let count = if slot == "mainhand" && m.species() == Species::Panda {
super::panda::held_count(m)
} else {
1
};
let mut stack = loot_stack(item?, count as i32);
if let (Some(index), Some(g)) = (index, gear::gear(m)) {
for (e, l) in &g.slots[index] {
stack.enchantments.push((e.to_string(), *l as i32));
+2 -8
View File
@@ -1691,13 +1691,7 @@ impl<L: FinalizeLevel + ?Sized, R: RandomSource> Ctx<'_, L, R> {
let hidden = panda_gene(self.int(16));
m.extra.variant = main;
m.extra.variant2 = hidden;
let shown = panda_variant(main, hidden);
if shown == 5 {
set_base(m, Attr::MaxHealth, 10.0);
}
if shown == 1 {
set_base(m, Attr::MovementSpeed, f64::from(0.07f32));
}
super::panda::set_attributes(m);
self.ageable(m, g.or(Some(Group::Ageable(Ageable::chance(0.2)))))
}
S::PolarBear | S::Ocelot => {
@@ -1950,7 +1944,7 @@ fn set_memory(m: &mut Mob, name: &'static str, value: i64, ttl: i64) {
}
/// `Panda.Gene.getRandom` (enum order: normal, lazy, worried, playful,
/// brown, weak, aggressive).
fn panda_gene(roll: i32) -> u8 {
pub fn panda_gene(roll: i32) -> u8 {
match roll {
0 => 1,
1 => 2,
+845
View File
@@ -0,0 +1,845 @@
//! Foxes (`net.minecraft.world.entity.animal.fox.Fox`): the goal list, the
//! flags byte (sleeping, sitting, crouching, interested, pouncing,
//! faceplanted), stalking and pouncing, napping in the shade by day, seeking
//! shelter, perching, eating berries and the food held in the mouth.
//! Picking up items, strolling through villages and trusting players (with
//! defending them) are not ported yet.
use super::{
Mob, MobEvent, MobWorld, Seen, Species,
goals::{
Avoid, AvoidEntity, Breed, Cx, Float, FollowParent, Goal, JUMP, LOOK, LeapAtTarget,
LookAtPlayer, MOVE, MeleeAttack, MoveToBlock, NearestTarget, Panic, Prey, Stroll,
goal_clone, hide_pos, reduced,
},
living::wrap,
};
use horizon_core::{
geometry::{Axis, Vec3},
jmath,
random::RandomSource,
};
use horizon_world::registry::BlockStateId;
// Fox.DATA_FLAGS_ID, kept in `extra.ints[0]` (and sent to clients as is).
pub const SITTING: i32 = 1;
pub const CROUCHING: i32 = 4;
pub const INTERESTED: i32 = 8;
pub const POUNCING: i32 = 16;
pub const SLEEPING: i32 = 32;
pub const FACEPLANTED: i32 = 64;
pub const DEFENDING: i32 = 128;
/// `Fox.ticksSinceEaten`.
const TICKS_SINCE_EATEN: usize = 1;
/// `Fox.interestedAngle` and `crouchAmount` in `anim.v`, the previous tick's
/// values in `anim.vo`.
const INTERESTED_ANGLE: usize = 0;
const CROUCH_AMOUNT: usize = 1;
/// `Fox.MAX_CROUCH_AMOUNT`.
const MAX_CROUCH_AMOUNT: f32 = 5.0;
pub fn flag(m: &Mob, f: i32) -> bool {
m.extra.ints[0] & f != 0
}
fn set_flag(m: &mut Mob, f: i32, on: bool) {
if on {
m.extra.ints[0] |= f;
} else {
m.extra.ints[0] &= !f;
}
}
/// `Fox.clearStates`.
fn clear_states(m: &mut Mob) {
m.extra.ints[0] = 0;
}
/// `Fox.canMove`, which `FoxMoveControl` waits for.
pub fn can_move(m: &Mob) -> bool {
!flag(m, SLEEPING | SITTING | FACEPLANTED)
}
/// `FoxLookControl.resetXRotOnTick`: stalking and pouncing keep their pitch.
pub fn resets_x_rot(m: &Mob) -> bool {
!flag(m, POUNCING | CROUCHING | INTERESTED | FACEPLANTED)
}
/// `Fox.registerGoals` and `setTargetGoals` (the targets wait for the variant:
/// red foxes prefer chickens and rabbits, snow foxes fish).
pub fn register(m: &mut Mob) {
let countdown = m.random.next_int_bound(reduced(140)).unwrap_or(0);
let g = &mut m.ai.goals;
g.add(0, Wrap::new(Float).on_start(clear_states));
g.add(1, Faceplant { countdown: 0 });
g.add(2, Panic::new(2.2));
g.add(3, Wrap::new(Breed::new(1.0)).on_start(clear_states));
// AVOID_PLAYERS: not a sneaking player.
g.add(
4,
AvoidEntity::new(Avoid::Player, 16.0, 1.6, 1.4)
.player_when(|p| !p.sneaking && !p.spectator),
);
g.add(
4,
AvoidEntity::new(Avoid::Species(vec![Species::Wolf]), 8.0, 1.6, 1.4).seen_when(|s| !s.tame),
);
g.add(
4,
AvoidEntity::new(Avoid::Species(vec![Species::PolarBear]), 8.0, 1.6, 1.4),
);
g.add(5, StalkPrey);
g.add(6, Pounce);
g.add(6, SeekShelter::new(1.25));
g.add(
7,
Wrap::new(MeleeAttack::new(1.2, true))
.when(
|m| !flag(m, SITTING | SLEEPING | CROUCHING | FACEPLANTED),
false,
)
.on_start(|m| set_flag(m, INTERESTED, false)),
);
g.add(7, Sleep { countdown });
g.add(8, Wrap::new(FollowParent::new(1.25)).on_start(clear_states));
g.add(10, EatBerries::new());
g.add(10, LeapAtTarget::new(0.4));
g.add(11, Stroll::avoiding_water(1.0));
// FoxLookAtPlayerGoal: not with the nose in the snow or while stalking.
g.add(
12,
Wrap::new(LookAtPlayer::new(24.0)).when(|m| !flag(m, FACEPLANTED | INTERESTED), true),
);
g.add(13, Perch::default());
let red = |m: &Mob, _: &MobWorld| m.extra.variant == 0;
let snow = |m: &Mob, _: &MobWorld| m.extra.variant != 0;
let land = || NearestTarget::new(Prey::Filter(chicken_or_rabbit), false).interval(10);
let turtle = || NearestTarget::new(Prey::Filter(baby_turtle_on_land), false).interval(10);
let fish = || NearestTarget::new(Prey::Filter(schooling_fish), false).interval(20);
let t = &mut m.ai.targets;
t.add(4, land().gate(red));
t.add(4, turtle().gate(red));
t.add(6, fish().gate(red));
t.add(4, fish().gate(snow));
t.add(6, land().gate(snow));
t.add(6, turtle().gate(snow));
}
fn chicken_or_rabbit(s: &Seen) -> bool {
matches!(s.species, Species::Chicken | Species::Rabbit)
}
fn baby_turtle_on_land(s: &Seen) -> bool {
s.species == Species::Turtle && s.baby && !s.in_water
}
fn schooling_fish(s: &Seen) -> bool {
matches!(
s.species,
Species::Cod | Species::Salmon | Species::TropicalFish
)
}
/// The part of `Fox.tick` a client runs too: the head tilt eases toward the
/// interested flag and the crouch builds up to 5 while crouching.
pub fn client_tick(m: &mut Mob) {
let (interested, crouching) = (flag(m, INTERESTED), flag(m, CROUCHING));
let v = &mut m.anim.v;
m.anim.vo[INTERESTED_ANGLE] = v[INTERESTED_ANGLE];
v[INTERESTED_ANGLE] += (if interested { 1.0 } else { 0.0 } - v[INTERESTED_ANGLE]) * 0.4;
m.anim.vo[CROUCH_AMOUNT] = v[CROUCH_AMOUNT];
v[CROUCH_AMOUNT] = if crouching {
(v[CROUCH_AMOUNT] + 0.2).min(MAX_CROUCH_AMOUNT)
} else {
0.0
};
}
/// `Fox.isFullyCrouched`.
fn fully_crouched(m: &Mob) -> bool {
m.anim.v[CROUCH_AMOUNT] == MAX_CROUCH_AMOUNT
}
/// `Fox.tick` and the eating part of `Fox.aiStep`.
pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
client_tick(m);
if !m.alive() {
return;
}
m.extra.ints[TICKS_SINCE_EATEN] += 1;
if let Some(item) = m.extra.main_hand.clone()
&& eats(w, &item)
&& m.ai.target.is_none()
&& m.on_ground
&& !flag(m, SLEEPING)
{
let eaten = m.extra.ints[TICKS_SINCE_EATEN];
if eaten > 600 {
// ItemStack.finishUsingItem: the food is gone, a use remainder stays.
m.extra.main_hand = w
.items
.and_then(|i| i.items.get(&item))
.and_then(|i| i.use_remainder.as_deref())
.map(Into::into);
m.extra.ints[TICKS_SINCE_EATEN] = 0;
} else if eaten > 560 && m.random.next_float() < 0.1 {
let at = [m.position.x, m.position.y, m.position.z];
events.push(MobEvent::Sound("entity.fox.eat", at, 1.0, 1.0));
}
}
if !m.target_info(w).is_some_and(|t| t.alive) {
set_flag(m, CROUCHING | INTERESTED, false);
}
let in_water = m.fluids.in_water;
if in_water || m.ai.target.is_some() || w.thundering {
set_flag(m, SLEEPING, false);
}
if in_water || flag(m, SLEEPING) {
set_flag(m, SITTING, false);
}
// Faceplanted: crumbs of the snow now and then (level event 2001).
if flag(m, FACEPLANTED) && m.random.next_float() < 0.2 {
let at = [
m.position.x.floor() as i32,
m.position.y.floor() as i32,
m.position.z.floor() as i32,
];
events.push(MobEvent::LevelEvent {
id: 2001,
pos: at,
data: w.world.block(at[0], at[1], at[2]).0 as i32,
});
}
}
/// `Fox.aiStep` before the living step: a sleeping fox takes no input.
pub fn pre_ai_step(m: &mut Mob) {
if flag(m, SLEEPING) {
m.ai.jumping = false;
m.ai.xxa = 0.0;
m.ai.zza = 0.0;
}
}
/// `Fox.isConsumableFood`: the item has food and consumable components.
fn eats(w: &MobWorld, item: &str) -> bool {
w.items
.and_then(|i| i.items.get(item))
.is_some_and(|i| i.food.is_some() && i.consumable.is_some())
}
/// `Fox.FoxBehaviorGoal.hasShelter`: out of the sky's sight and not a bad spot.
fn has_shelter(m: &Mob, w: &MobWorld) -> bool {
let at = [
m.position.x.floor() as i32,
m.bounds().max(Axis::Y).floor() as i32,
m.position.z.floor() as i32,
];
!w.can_see_sky(at[0], at[1], at[2]) && m.walk_target_value(w, at) >= 0.0
}
/// `Fox.FoxBehaviorGoal.alertable`: someone within 12 blocks the fox would
/// keep watch over (`FoxAlertableEntitiesSelector`).
fn alertable(m: &Mob, w: &MobWorld) -> bool {
let (from, id, y) = (m.position, m.id, m.position.y);
let mob = w
.nearby(from, 12.0, |s: &Seen| {
s.id != id
&& (s.position.y - y).abs() <= 6.0
&& m.distance_sqr(s.position) <= 144.0
&& match s.species {
Species::Fox => false,
Species::Chicken | Species::Rabbit => true,
Species::Wolf | Species::Cat | Species::Parrot | Species::Nautilus => !s.tame,
_ => true,
}
})
.next()
.is_some();
mob || w.player.as_ref().is_some_and(|p| {
p.alive
&& !p.creative
&& !p.spectator
&& !p.sneaking
&& (p.position.y - y).abs() <= 6.0
&& m.distance_sqr(p.position) <= 144.0
})
}
/// `Fox.SleepGoal`: nap in the shade by day when nothing is around.
#[derive(Clone, Debug)]
struct Sleep {
countdown: i32,
}
impl Sleep {
fn can_sleep(&mut self, m: &Mob, w: &MobWorld) -> bool {
if self.countdown > 0 {
self.countdown -= 1;
return false;
}
let (x, y, z) = (
m.position.x.floor() as i32,
m.position.y.floor() as i32,
m.position.z.floor() as i32,
);
w.daylight
&& has_shelter(m, w)
&& !alertable(m, w)
&& w.block_name(x, y, z) != "minecraft:powder_snow"
}
}
impl Goal for Sleep {
goal_clone!();
fn flags(&self) -> u8 {
MOVE | LOOK | JUMP
}
fn can_use(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
m.ai.nav.is_done() && (self.can_sleep(m, cx.w) || flag(m, SLEEPING))
}
fn can_continue(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
self.can_sleep(m, cx.w)
}
fn start(&mut self, m: &mut Mob, _: &mut Cx) {
set_flag(m, SITTING | CROUCHING | INTERESTED, false);
m.ai.jumping = false;
set_flag(m, SLEEPING, true);
m.nav_stop();
let p = m.position;
m.ai.mov.set_wanted(p.x, p.y, p.z, 0.0);
}
fn stop(&mut self, m: &mut Mob, _: &mut Cx) {
self.countdown = m.random.next_int_bound(reduced(140)).unwrap_or(0);
clear_states(m);
}
}
/// A shared goal under the fox's own conditions and start-up: the extra
/// `canUse` of `FoxMeleeAttackGoal` and `FoxLookAtPlayerGoal` and the
/// `clearStates` or `setIsInterested(false)` its subclasses do in `start`.
#[derive(Clone, Debug)]
struct Wrap<G> {
goal: G,
when: Option<fn(&Mob) -> bool>,
/// The condition also applies to `canContinueToUse`.
both: bool,
on_start: Option<fn(&mut Mob)>,
}
impl<G: Goal + Clone + 'static> Wrap<G> {
fn new(goal: G) -> Self {
Self {
goal,
when: None,
both: false,
on_start: None,
}
}
fn when(mut self, f: fn(&Mob) -> bool, both: bool) -> Self {
(self.when, self.both) = (Some(f), both);
self
}
fn on_start(mut self, f: fn(&mut Mob)) -> Self {
self.on_start = Some(f);
self
}
}
impl<G: Goal + Clone + 'static> Goal for Wrap<G> {
goal_clone!();
fn flags(&self) -> u8 {
self.goal.flags()
}
fn can_use(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
self.when.is_none_or(|f| f(m)) && self.goal.can_use(m, cx)
}
fn can_continue(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
(!self.both || self.when.is_none_or(|f| f(m))) && self.goal.can_continue(m, cx)
}
fn interruptable(&self) -> bool {
self.goal.interruptable()
}
fn start(&mut self, m: &mut Mob, cx: &mut Cx) {
if let Some(f) = self.on_start {
f(m);
}
self.goal.start(m, cx);
}
fn stop(&mut self, m: &mut Mob, cx: &mut Cx) {
self.goal.stop(m, cx);
}
fn every_tick(&self) -> bool {
self.goal.every_tick()
}
fn tick(&mut self, m: &mut Mob, cx: &mut Cx) {
self.goal.tick(m, cx);
}
fn name(&self) -> &'static str {
self.goal.name()
}
}
/// `Fox.isPathClear`: six points along the way to the target, each with
/// nothing but replaceable blocks in the three above it.
fn path_clear(m: &Mob, w: &MobWorld, to: Vec3) -> bool {
let (zdiff, xdiff) = (to.z - m.position.z, to.x - m.position.x);
let slope = zdiff / xdiff;
for i in 0..6 {
let step = f64::from(i as f32 / 6.0);
let z = if slope == 0.0 { 0.0 } else { zdiff * step };
let x = if slope == 0.0 {
xdiff * step
} else {
z / slope
};
for j in 1..4 {
let at = [
(m.position.x + x).floor() as i32,
(m.position.y + f64::from(j)).floor() as i32,
(m.position.z + z).floor() as i32,
];
if !w.facts.get(w.world.block(at[0], at[1], at[2])).replaceable {
return false;
}
}
}
true
}
fn signum(x: f64) -> f64 {
if x > 0.0 {
1.0
} else if x < 0.0 {
-1.0
} else {
0.0
}
}
/// `Fox.FaceplantGoal`: stay put for 40 ticks with the nose in the snow.
#[derive(Clone, Debug)]
struct Faceplant {
countdown: i32,
}
impl Goal for Faceplant {
goal_clone!();
fn flags(&self) -> u8 {
MOVE | LOOK | JUMP
}
fn can_use(&mut self, m: &mut Mob, _: &mut Cx) -> bool {
flag(m, FACEPLANTED)
}
fn can_continue(&mut self, m: &mut Mob, _: &mut Cx) -> bool {
flag(m, FACEPLANTED) && self.countdown > 0
}
fn start(&mut self, _: &mut Mob, _: &mut Cx) {
self.countdown = reduced(40);
}
fn stop(&mut self, m: &mut Mob, _: &mut Cx) {
set_flag(m, FACEPLANTED, false);
}
fn tick(&mut self, _: &mut Mob, _: &mut Cx) {
self.countdown -= 1;
}
}
/// `Fox.StalkPreyGoal`: creep up on a chicken or rabbit more than 6 blocks
/// away, then crouch and keep still once it is within 6.
#[derive(Clone, Debug)]
struct StalkPrey;
impl StalkPrey {
fn look(m: &mut Mob, eye: [f64; 3]) {
let (y, x) = (m.kind.max_head_y_rot, m.kind.max_head_x_rot);
m.look_at_speed(eye, y, x);
}
}
impl Goal for StalkPrey {
goal_clone!();
fn flags(&self) -> u8 {
MOVE | LOOK
}
fn can_use(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
if flag(m, SLEEPING) {
return false;
}
let Some(t) = m.target_info(cx.w) else {
return false;
};
let stalkable = match m.ai.target {
Some(super::Who::Mob(id)) => {
cx.w.seen(id)
.is_some_and(|s| matches!(s.species, Species::Chicken | Species::Rabbit))
}
_ => false,
};
t.alive
&& stalkable
&& m.distance_sqr(t.position) > 36.0
&& !flag(m, CROUCHING | INTERESTED)
&& !m.ai.jumping
}
fn start(&mut self, m: &mut Mob, _: &mut Cx) {
set_flag(m, SITTING | FACEPLANTED, false);
}
fn stop(&mut self, m: &mut Mob, cx: &mut Cx) {
match m.target_info(cx.w) {
Some(t) if path_clear(m, cx.w, t.position) => {
set_flag(m, INTERESTED | CROUCHING, true);
m.nav_stop();
Self::look(m, [t.position.x, t.eye_y, t.position.z]);
}
_ => set_flag(m, INTERESTED | CROUCHING, false),
}
}
fn tick(&mut self, m: &mut Mob, cx: &mut Cx) {
let Some(t) = m.target_info(cx.w) else { return };
Self::look(m, [t.position.x, t.eye_y, t.position.z]);
if m.distance_sqr(t.position) <= 36.0 {
set_flag(m, INTERESTED | CROUCHING, true);
m.nav_stop();
} else {
let at = [t.position.x, t.position.y, t.position.z];
m.nav_move_to_entity(cx.w, at, 0, 1.5);
}
}
}
/// `Fox.FoxPounceGoal`: once fully crouched with a clear path, leap at the
/// target, tilt with the flight and bite when within 2 blocks, or end up with
/// the nose in snow.
#[derive(Clone, Debug)]
struct Pounce;
impl Goal for Pounce {
goal_clone!();
fn flags(&self) -> u8 {
MOVE | JUMP
}
fn can_use(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
if !fully_crouched(m) {
return false;
}
let Some(t) = m.target_info(cx.w).filter(|t| t.alive) else {
return false;
};
let clear = path_clear(m, cx.w, t.position);
if !clear {
set_flag(m, CROUCHING | INTERESTED, false);
}
clear
}
fn can_continue(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
if !m.target_info(cx.w).is_some_and(|t| t.alive) {
return false;
}
let yd = m.velocity.y;
(yd * yd >= f64::from(0.05f32) || m.head_pitch.abs() >= 15.0 || !m.on_ground)
&& !flag(m, FACEPLANTED)
}
fn interruptable(&self) -> bool {
false
}
fn start(&mut self, m: &mut Mob, cx: &mut Cx) {
m.ai.jumping = true;
set_flag(m, POUNCING, true);
set_flag(m, INTERESTED, false);
if let Some(t) = m.target_info(cx.w) {
m.look_at_speed([t.position.x, t.eye_y, t.position.z], 60.0, 30.0);
let u = m.position.vector_to(t.position).normalize();
m.velocity = m.velocity.add_vector(Vec3::new(u.x * 0.8, 0.9, u.z * 0.8));
}
m.nav_stop();
}
fn stop(&mut self, m: &mut Mob, _: &mut Cx) {
set_flag(m, CROUCHING | INTERESTED | POUNCING, false);
m.anim.v[CROUCH_AMOUNT] = 0.0;
m.anim.vo[CROUCH_AMOUNT] = 0.0;
}
fn tick(&mut self, m: &mut Mob, cx: &mut Cx) {
let target = m.target_info(cx.w);
if let Some(t) = &target {
m.look_at_speed([t.position.x, t.eye_y, t.position.z], 60.0, 30.0);
}
if !flag(m, FACEPLANTED) {
let v = m.velocity;
if v.y * v.y < f64::from(0.03f32) && m.head_pitch != 0.0 {
m.head_pitch += 0.2 * wrap(-m.head_pitch);
} else {
let horizontal = v.horizontal_distance();
let bias = if m.ai.jumping && v.y > 0.0 { 6.5 } else { 1.0 };
let y = v.y * bias;
let len = (horizontal * horizontal + y * y).sqrt();
if len > f64::from(1.0e-5f32) {
let pitch = signum(-y) * jmath::acos(horizontal / len) * 180.0
/ f64::from(std::f32::consts::PI);
m.head_pitch = (pitch as f32 % 360.0).clamp(-90.0, 90.0);
}
}
}
if let (Some(t), Some(who)) = (&target, m.ai.target)
&& m.distance_sqr(t.position) <= 4.0
{
super::ai::do_hurt_target(m, who, cx);
} else if m.head_pitch > 0.0
&& m.on_ground
&& m.velocity.y as f32 != 0.0
&& cx.w.block_name(
m.position.x.floor() as i32,
m.position.y.floor() as i32,
m.position.z.floor() as i32,
) == "minecraft:snow"
{
m.head_pitch = 60.0;
m.ai.target = None;
set_flag(m, FACEPLANTED, true);
}
}
}
/// `Fox.SeekShelterGoal`: by day, or in a thunderstorm, leave open sky for
/// somewhere the sky does not see. Villages are not told apart yet (vanilla
/// stays put in one).
#[derive(Clone, Debug)]
struct SeekShelter {
speed: f64,
interval: i32,
wanted: [f64; 3],
}
impl SeekShelter {
fn new(speed: f64) -> Self {
Self {
speed,
interval: reduced(100),
wanted: [0.0; 3],
}
}
fn set_wanted_pos(&mut self, m: &mut Mob, w: &MobWorld) -> bool {
hide_pos(m, w).is_some_and(|at| {
self.wanted = at;
true
})
}
}
impl Goal for SeekShelter {
goal_clone!();
fn flags(&self) -> u8 {
MOVE
}
fn can_use(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
if flag(m, SLEEPING) || m.ai.target.is_some() {
return false;
}
let at = [
m.position.x.floor() as i32,
m.position.y.floor() as i32,
m.position.z.floor() as i32,
];
let sees_sky = cx.w.can_see_sky(at[0], at[1], at[2]);
if cx.w.thundering && sees_sky {
self.set_wanted_pos(m, cx.w)
} else if self.interval > 0 {
self.interval -= 1;
false
} else {
self.interval = 100;
cx.w.daylight && sees_sky && self.set_wanted_pos(m, cx.w)
}
}
fn can_continue(&mut self, m: &mut Mob, _: &mut Cx) -> bool {
!m.ai.nav.is_done()
}
fn start(&mut self, m: &mut Mob, cx: &mut Cx) {
clear_states(m);
m.nav_move_to(cx.w, self.wanted, self.speed);
}
}
/// `Fox.PerchAndSearchGoal`: now and then sit down and look around two to
/// four times when nothing is going on.
#[derive(Clone, Debug, Default)]
struct Perch {
rel: [f64; 2],
look_time: i32,
looks_remaining: i32,
}
impl Perch {
fn reset_look(&mut self, m: &mut Mob) {
let angle = std::f64::consts::TAU * m.random.next_double();
self.rel = [angle.cos(), angle.sin()];
self.look_time = reduced(80 + m.random.next_int_bound(20).unwrap_or(0));
}
}
impl Goal for Perch {
goal_clone!();
fn flags(&self) -> u8 {
MOVE | LOOK
}
fn can_use(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
m.ai.last_hurt_by.is_none()
&& m.random.next_float() < 0.02
&& !flag(m, SLEEPING)
&& m.ai.target.is_none()
&& m.ai.nav.is_done()
&& !alertable(m, cx.w)
&& !flag(m, POUNCING | CROUCHING)
}
fn can_continue(&mut self, _: &mut Mob, _: &mut Cx) -> bool {
self.looks_remaining > 0
}
fn start(&mut self, m: &mut Mob, _: &mut Cx) {
self.reset_look(m);
self.looks_remaining = 2 + m.random.next_int_bound(3).unwrap_or(0);
set_flag(m, SITTING, true);
m.nav_stop();
}
fn stop(&mut self, m: &mut Mob, _: &mut Cx) {
set_flag(m, SITTING, false);
}
fn tick(&mut self, m: &mut Mob, _: &mut Cx) {
self.look_time -= 1;
if self.look_time <= 0 {
self.looks_remaining -= 1;
self.reset_look(m);
}
let at = [
m.position.x + self.rel[0],
m.eye_y(),
m.position.z + self.rel[1],
];
let (y, x) = (m.kind.max_head_y_rot, m.kind.max_head_x_rot);
m.look_at_speed(at, y, x);
}
}
/// `BlockState` with one property changed.
fn with_prop(w: &MobWorld, p: [i32; 3], key: &str, value: &str) -> Option<BlockStateId> {
let mut s = w.registry.state(w.world.block(p[0], p[1], p[2]))?.clone();
s.properties.insert(key.into(), value.into());
w.registry.id(&s)
}
/// `FoxEatBerriesGoal.isValidTarget`: a ripe sweet berry bush or glow berries.
fn has_berries(w: &MobWorld, p: [i32; 3]) -> bool {
match w.block_name(p[0], p[1], p[2]) {
"minecraft:sweet_berry_bush" => w
.prop(p[0], p[1], p[2], "age")
.and_then(|a| a.parse::<i32>().ok())
.is_some_and(|a| a >= 2),
"minecraft:cave_vines" | "minecraft:cave_vines_plant" => {
w.prop(p[0], p[1], p[2], "berries") == Some("true")
}
_ => false,
}
}
/// `Fox.FoxEatBerriesGoal`: walk to a ripe berry bush and pick it after 40 ticks.
#[derive(Clone, Debug)]
struct EatBerries {
block: MoveToBlock,
waited: i32,
}
impl EatBerries {
fn new() -> Self {
Self {
block: MoveToBlock::new(1.2, 12, 1),
waited: 0,
}
}
fn pick(&mut self, m: &mut Mob, cx: &mut Cx) {
let p = self.block.block;
let at = [
f64::from(p[0]) + 0.5,
f64::from(p[1]) + 0.5,
f64::from(p[2]) + 0.5,
];
if cx.w.block_name(p[0], p[1], p[2]) == "minecraft:sweet_berry_bush" {
let age: i32 =
cx.w.prop(p[0], p[1], p[2], "age")
.and_then(|a| a.parse().ok())
.unwrap_or(0);
let mut count = 1 + m.random.next_int_bound(2).unwrap_or(0) + i32::from(age == 3);
if m.extra.main_hand.is_none() {
m.extra.main_hand = Some("minecraft:sweet_berries".into());
count -= 1;
}
if count > 0 {
cx.events.push(MobEvent::DropItem {
item: "minecraft:sweet_berries".into(),
count: count as u32,
at,
});
}
if let Some(state) = with_prop(cx.w, p, "age", "1") {
cx.events.push(MobEvent::SetBlock { pos: p, state });
}
cx.events.push(MobEvent::Sound(
"block.sweet_berry_bush.pick_berries",
at,
1.0,
1.0,
));
} else {
// CaveVines.use: one glow berry, the vine goes back to bare.
cx.events.push(MobEvent::DropItem {
item: "minecraft:glow_berries".into(),
count: 1,
at,
});
if let Some(state) = with_prop(cx.w, p, "berries", "false") {
cx.events.push(MobEvent::SetBlock { pos: p, state });
}
let pitch = 0.8 + m.random.next_float() * 0.4;
cx.events.push(MobEvent::Sound(
"block.cave_vines.pick_berries",
at,
1.0,
pitch,
));
}
}
}
impl Goal for EatBerries {
goal_clone!();
fn flags(&self) -> u8 {
MOVE | JUMP
}
fn can_use(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
if flag(m, SLEEPING) {
return false;
}
// MoveToBlockGoal.canUse draws the next delay only once the last one
// has run out.
if self.block.next_start > 0 {
self.block.next_start -= 1;
return false;
}
let next = reduced(200 + m.random.next_int_bound(200).unwrap_or(0));
self.block.can_use(m, cx.w, next, has_berries)
}
fn can_continue(&mut self, _: &mut Mob, cx: &mut Cx) -> bool {
self.block.can_continue(cx.w, has_berries)
}
fn start(&mut self, m: &mut Mob, cx: &mut Cx) {
self.waited = 0;
set_flag(m, SITTING, false);
self.block.start(m, cx.w);
}
fn every_tick(&self) -> bool {
true
}
fn tick(&mut self, m: &mut Mob, cx: &mut Cx) {
if self.block.reached {
if self.waited >= 40 {
if cx.w.mob_griefing {
self.pick(m, cx);
}
} else {
self.waited += 1;
}
} else if m.random.next_float() < 0.05 {
let at = [m.position.x, m.position.y, m.position.z];
cx.events
.push(MobEvent::Sound("entity.fox.sniff", at, 1.0, 1.0));
}
self.block.tick_within(m, cx.w, 2.0);
}
}
/// `FoxRenderState` as the renderer reads it: the flags, `getHeadRollAngle`
/// and `getCrouchAmount` at the partial tick. `FoxModel.legMotionPos` gains
/// 0.67 a frame; this takes 60 frames a second.
pub fn pose(m: &Mob, partial: f32) -> super::body_anim::Fox {
super::body_anim::Fox {
crouching: flag(m, CROUCHING),
sleeping: flag(m, SLEEPING),
sitting: flag(m, SITTING),
pouncing: flag(m, POUNCING),
faceplanted: flag(m, FACEPLANTED),
head_roll_angle: m.anim.lerp(INTERESTED_ANGLE, partial) * 0.11 * std::f32::consts::PI,
crouch_amount: m.anim.lerp(CROUCH_AMOUNT, partial),
leg_motion_pos: (m.age as f32 + partial) * 0.67 * 3.0,
}
}
+312
View File
@@ -0,0 +1,312 @@
//! Frogs (`Frog`, `FrogAi`): the `Croak` and `ShootTongue` behaviours as
//! goals, the poses they set and the client's animation states.
//! AnimState states: jump, croak, tongue, swim idle.
//!
//! The brain's idle `RunOne` picks a croak three times as often as a stroll
//! (weights 3 and 1; it also holds a do-nothing option that only repeats
//! the pick), so a frog that is not walking croaks in three of four idle
//! picks: `Croak` takes those, and the frog's stroll then follows at once.
use super::{
Mob, MobEvent, MobWorld, Species, Who,
goals::{
Cx, Float, Goal, LOOK, LookAtPlayer, MOVE, Panic, RandomLookAround, Stroll, goal_clone,
},
sync,
};
use horizon_core::{geometry::Vec3, random::RandomSource};
const JUMP: usize = 0;
const CROAK: usize = 1;
const TONGUE: usize = 2;
const SWIM_IDLE: usize = 3;
/// `Croak.CROAK_TICKS`.
const CROAK_TICKS: i32 = 60;
/// `ShootTongue`'s duration and eating distance.
const TONGUE_TIME_OUT: i32 = 100;
const EATING_DISTANCE: f64 = 1.75;
/// `FrogAttackablesSensor.TARGET_DETECTION_DISTANCE` and the sensor's scan
/// rate.
const DETECTION_DISTANCE: f64 = 10.0;
const SCAN_RATE: i32 = 20;
/// `ShootTongue.State`.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
enum Phase {
MoveToTarget,
CatchAnimation,
EatAnimation,
#[default]
Done,
}
/// The frog's brain memories and `DATA_TONGUE_TARGET_ID`.
#[derive(Clone, Debug, Default)]
pub struct State {
croak: i32,
/// Ticks to the attackables sensor's next scan, and `NEAREST_ATTACKABLE`.
scan: i32,
nearest: Option<u32>,
/// `UNREACHABLE_TONGUE_TARGETS`: the mob and the ticks it stays in mind.
unreachable: Vec<(u32, i32)>,
/// `ATTACK_TARGET` while `ShootTongue` runs, and the synched tongue target.
target: Option<u32>,
pub tongue_target: Option<u32>,
phase: Phase,
eat_timer: i32,
path_counter: i32,
time: i32,
}
pub fn register(m: &mut Mob) {
// `Frog()`: SmoothSwimmingMoveControl(85, 10, 0.02, 0.1, true). The
// frog's movement speed is 1.0, so the controller's 0.1 outside-water
// speed is what keeps a walking frog at vanilla's pace; the plain ground
// controller walks ten times too fast (#2213).
m.ai.mov.kind = super::control::MoveKind::SmoothSwimming {
max_turn_x: 85.0,
max_turn_y: 10.0,
in_water: 0.02,
outside: 0.1,
gravity: true,
};
let g = &mut m.ai.goals;
g.add(0, Float);
g.add(1, Panic::new(1.25));
g.add(2, ShootTongue);
g.add(4, Croak);
g.add(5, Stroll::avoiding_water(1.0).interval(1));
g.add(6, LookAtPlayer::new(6.0));
g.add(7, RandomLookAround::default());
}
/// `Entity.setPose` and `Frog.onSyncedDataUpdated(DATA_POSE)`.
pub fn set_pose(m: &mut Mob, id: u8) {
let old = std::mem::replace(&mut m.extra.pose, id);
if old != id {
pose_changed(m);
}
}
/// The animation states of the pose the frog has taken.
pub fn pose_changed(m: &mut Mob) {
let age = m.age as f32;
let pose = m.extra.pose;
let states = &mut m.anim.states;
for (state, on) in [
(JUMP, pose == sync::LONG_JUMPING),
(CROAK, pose == sync::CROAKING),
(TONGUE, pose == sync::USING_TONGUE),
] {
if on {
states[state].start(age);
} else {
states[state].stop();
}
}
}
/// `Frog.tick` on a client: the idle swim while it floats still. The server
/// runs it too, for a window that shows its own mobs.
pub fn tick(m: &mut Mob) {
let age = m.age as f32;
let still = m.fluids.in_water && m.limb_speed <= 1.0e-5;
m.anim.states[SWIM_IDLE].animate_when(still, age);
}
/// `Frog.canEat`: a slime or magma cube of size 1.
fn can_eat(species: Species, size: i32) -> bool {
matches!(species, Species::Slime | Species::MagmaCube) && size == 1
}
/// `FrogAttackablesSensor` (every 20 ticks) and the memory timers.
pub fn server_ai_step(m: &mut Mob, w: &MobWorld) {
let f = &mut m.extra.frog;
f.unreachable.retain_mut(|(_, ttl)| {
*ttl -= 1;
*ttl > 0
});
f.scan -= 1;
if f.scan > 0 {
return;
}
f.scan = SCAN_RATE;
let unreachable: Vec<u32> = f.unreachable.iter().map(|u| u.0).collect();
let eye = Vec3::new(m.position.x, m.eye_y(), m.position.z);
let nearest = w
.nearby(m.position, DETECTION_DISTANCE, |s| {
can_eat(s.species, s.size)
&& !unreachable.contains(&s.id)
&& w.line_of_sight(eye, Vec3::new(s.position.x, s.eye_y, s.position.z))
})
.find(|s| s.position.distance_squared(m.position) < DETECTION_DISTANCE * DETECTION_DISTANCE)
.map(|s| s.id);
m.extra.frog.nearest = nearest;
}
/// `Croak`: the pose sync::CROAKING for 60 ticks (the croak is the frog's ambient
/// sound).
#[derive(Clone, Debug)]
struct Croak;
impl Goal for Croak {
goal_clone!();
fn flags(&self) -> u8 {
MOVE
}
fn can_use(&mut self, m: &mut Mob, _cx: &mut Cx) -> bool {
// checkExtraStartConditions (standing), no walk target, and not in
// a liquid (start does nothing there).
m.extra.pose == sync::STANDING
&& m.on_ground
&& !m.fluids.in_water
&& !m.fluids.in_lava
&& m.ai.nav.is_done()
&& m.random.next_int_bound(4).unwrap_or(0) != 0
}
fn can_continue(&mut self, m: &mut Mob, _cx: &mut Cx) -> bool {
m.extra.frog.croak < CROAK_TICKS
}
fn start(&mut self, m: &mut Mob, _cx: &mut Cx) {
m.extra.frog.croak = 0;
set_pose(m, sync::CROAKING);
}
fn stop(&mut self, m: &mut Mob, _cx: &mut Cx) {
set_pose(m, sync::STANDING);
}
fn tick(&mut self, m: &mut Mob, _cx: &mut Cx) {
m.extra.frog.croak += 1;
}
fn every_tick(&self) -> bool {
true
}
}
/// `ShootTongue` with the tongue activity's `StartAttacking` and
/// `StopAttackingIfTargetInvalid`: walk up to a small slime or magma cube,
/// shoot the tongue, pull it in and eat it.
#[derive(Clone, Debug)]
struct ShootTongue;
impl Goal for ShootTongue {
goal_clone!();
fn flags(&self) -> u8 {
MOVE | LOOK
}
fn can_use(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
let Some(id) = m.extra.frog.nearest else {
return false;
};
// The start conditions: no walk target, no panic, not croaking.
if m.ai.panicking || m.extra.pose == sync::CROAKING || !m.ai.nav.is_done() {
return false;
}
let Some(t) = cx.w.seen(id).filter(|t| t.alive) else {
m.extra.frog.nearest = None;
return false;
};
// canPathfindToTarget: a path that ends at the target.
let at = [t.position.x, t.position.y, t.position.z];
let ends = m
.create_path_to_entity(cx.w, at, 0)
.is_some_and(|p| f64::from(p.dist_to_target) < EATING_DISTANCE);
if !ends {
// The target is forgotten and not taken for 100 ticks.
let f = &mut m.extra.frog;
f.nearest = None;
if !f.unreachable.iter().any(|u| u.0 == id) {
if f.unreachable.len() == 5 {
f.unreachable.remove(0);
}
f.unreachable.push((id, 100));
}
return false;
}
m.extra.frog.target = Some(id);
true
}
fn can_continue(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
let f = &m.extra.frog;
f.phase != Phase::Done
&& f.time <= TONGUE_TIME_OUT
&& !m.ai.panicking
&& f.target
.is_some_and(|id| cx.w.seen(id).is_some_and(|t| t.alive))
}
fn start(&mut self, m: &mut Mob, cx: &mut Cx) {
let Some(t) = m.extra.frog.target.and_then(|id| cx.w.seen(id)) else {
return;
};
let at = [t.position.x, t.position.y, t.position.z];
let eye = [t.position.x, t.eye_y, t.position.z];
m.look_at(eye);
m.extra.frog.tongue_target = m.extra.frog.target;
m.nav_move_to_entity(cx.w, at, 0, 2.0);
let f = &mut m.extra.frog;
f.path_counter = 10;
f.time = 0;
f.phase = Phase::MoveToTarget;
}
fn stop(&mut self, m: &mut Mob, _cx: &mut Cx) {
let f = &mut m.extra.frog;
f.target = None;
f.tongue_target = None;
f.phase = Phase::Done;
set_pose(m, sync::STANDING);
}
fn every_tick(&self) -> bool {
true
}
fn tick(&mut self, m: &mut Mob, cx: &mut Cx) {
let Some(id) = m.extra.frog.target else {
return;
};
let Some(t) = cx.w.seen(id) else {
return;
};
let (at, pos) = ([t.position.x, t.position.y, t.position.z], t.position);
m.look_at([t.position.x, t.eye_y, t.position.z]);
m.extra.frog.tongue_target = Some(id);
m.extra.frog.time += 1;
let here = [m.position.x, m.position.y, m.position.z];
match m.extra.frog.phase {
Phase::MoveToTarget => {
if pos.distance_squared(m.position) < EATING_DISTANCE * EATING_DISTANCE {
cx.events
.push(MobEvent::Sound("entity.frog.tongue", here, 2.0, 1.0));
set_pose(m, sync::USING_TONGUE);
// The target is drawn in at 0.75 a tick.
let pull = m.position.subtract(pos).normalize().scale(0.75);
cx.events.push(MobEvent::Push {
target: Who::Mob(id),
velocity: [pull.x, pull.y, pull.z],
});
let f = &mut m.extra.frog;
f.eat_timer = 0;
f.phase = Phase::CatchAnimation;
} else if m.extra.frog.path_counter <= 0 {
m.nav_move_to_entity(cx.w, at, 0, 2.0);
m.extra.frog.path_counter = 10;
} else {
m.extra.frog.path_counter -= 1;
}
}
Phase::CatchAnimation => {
let f = &mut m.extra.frog;
f.eat_timer += 1;
if f.eat_timer > 6 {
f.phase = Phase::EatAnimation;
// eatEntity.
cx.events
.push(MobEvent::Sound("entity.frog.eat", here, 2.0, 1.0));
super::ai::do_hurt_target(m, Who::Mob(id), cx);
}
}
Phase::EatAnimation => {
let f = &mut m.extra.frog;
if f.eat_timer >= 10 {
f.phase = Phase::Done;
} else {
f.eat_timer += 1;
}
}
Phase::Done => {}
}
}
}
+66 -15
View File
@@ -673,12 +673,15 @@ impl Goal for Float {
#[derive(Clone, Debug)]
pub struct Panic {
pub speed: f64,
/// Only the damage of `#panic_environmental_causes` (an armadillo).
pub environmental: bool,
pos: [f64; 3],
}
impl Panic {
pub fn new(speed: f64) -> Self {
Self {
speed,
environmental: false,
pos: [0.0; 3],
}
}
@@ -694,7 +697,14 @@ impl Goal for Panic {
MOVE
}
fn can_use(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
if !should_panic(m) {
let wanted = if self.environmental {
m.last_damage.is_some_and(|(d, t)| {
super::armadillo::environmental(d.kind) && i64::from(m.age) - t <= 40
}) || m.fire_ticks > 0 && !m.kind.flags.fire_immune
} else {
should_panic(m)
};
if !wanted {
return false;
}
if m.fire_ticks > 0 {
@@ -1461,6 +1471,9 @@ pub struct HurtByTarget {
/// `Bee.BeeHurtByOtherGoal`: kept only while angry, and alerting others
/// only when the attacker is in sight.
pub bee: bool,
/// Whom `alertOther` calls: a filter on the mob (`PandaHurtByTargetGoal`
/// calls aggressive pandas only).
pub alert_if: Option<fn(&Seen) -> bool>,
/// `PolarBear.PolarBearHurtByTargetGoal`: a hurt cub only calls the
/// adult bears and drops the target itself.
pub polar_bear: bool,
@@ -1474,6 +1487,7 @@ impl HurtByTarget {
alert_except: vec![],
ignore: vec![],
bee: false,
alert_if: None,
polar_bear: false,
timestamp: 0,
st: TargetState {
@@ -1494,6 +1508,10 @@ impl HurtByTarget {
self.bee = true;
self
}
pub fn alert_if(mut self, f: fn(&Seen) -> bool) -> Self {
self.alert_if = Some(f);
self
}
pub fn polar_bear(mut self) -> Self {
self.polar_bear = true;
self
@@ -1571,6 +1589,7 @@ impl Goal for HurtByTarget {
&& (s.position.z - pos.z).abs() <= range + 1.0
})
.filter(|s| !self.alert_except.contains(&s.species))
.filter(|s| self.alert_if.is_none_or(|f| f(s)))
// PolarBearHurtByTargetGoal.alertOther: adults only.
.filter(|s| !(self.polar_bear && s.baby))
.filter(|s| Who::Mob(s.id) != by)
@@ -1610,6 +1629,10 @@ pub struct AvoidEntity {
pub sprint: f64,
/// Extra condition on the avoider (e.g. untamed cats only).
pub when: Option<fn(&Mob) -> bool>,
/// Extra condition on the player or mob avoided (`AvoidEntityGoal`'s
/// predicate: a sneaking player, a tame wolf).
pub player_when: Option<fn(&super::PlayerView) -> bool>,
pub seen_when: Option<fn(&Seen) -> bool>,
from: Vec3,
/// The mob being avoided (`toAvoid`), when it is one.
pub found: Option<u32>,
@@ -1622,6 +1645,8 @@ impl AvoidEntity {
walk,
sprint,
when: None,
player_when: None,
seen_when: None,
from: Vec3::ZERO,
found: None,
}
@@ -1630,6 +1655,14 @@ impl AvoidEntity {
self.when = Some(f);
self
}
pub fn player_when(mut self, f: fn(&super::PlayerView) -> bool) -> Self {
self.player_when = Some(f);
self
}
pub fn seen_when(mut self, f: fn(&Seen) -> bool) -> Self {
self.seen_when = Some(f);
self
}
}
impl Goal for AvoidEntity {
goal_clone!();
@@ -1650,6 +1683,7 @@ impl Goal for AvoidEntity {
.filter(|p| {
p.alive
&& !p.creative
&& self.player_when.is_none_or(|f| f(p))
&& m.distance_sqr(p.position) <= max * max
&& (p.position.y - m.position.y).abs() <= max + 3.0
})
@@ -1658,10 +1692,12 @@ impl Goal for AvoidEntity {
Avoid::Species(list) => {
let id = m.id;
let y = m.position.y;
let seen_when = self.seen_when;
let hit =
cx.w.nearby(m.position, max, move |s| {
s.id != id
&& list.contains(&s.species)
&& seen_when.is_none_or(|f| f(s))
&& (s.position.y - y).abs() <= 3.0 + max
})
.map(|s| (s.id, s.position))
@@ -2040,6 +2076,30 @@ impl Goal for RestrictSun {
m.ai.nav.avoid_sun = false;
}
}
/// `FleeSunGoal.getHidePos`: a spot within 10 blocks that the sky does not
/// see and the mob likes.
pub fn hide_pos(m: &mut Mob, w: &MobWorld) -> Option<[f64; 3]> {
let (bx, by, bz) = (
m.position.x.floor() as i32,
m.position.y.floor() as i32,
m.position.z.floor() as i32,
);
for _ in 0..10 {
let p = [
bx + m.random.next_int_bound(20).unwrap_or(0) - 10,
by + m.random.next_int_bound(6).unwrap_or(0) - 3,
bz + m.random.next_int_bound(20).unwrap_or(0) - 10,
];
if !w.can_see_sky(p[0], p[1], p[2]) && m.walk_target_value(w, p) < 0.0 {
return Some([
f64::from(p[0]) + 0.5,
f64::from(p[1]),
f64::from(p[2]) + 0.5,
]);
}
}
None
}
/// FleeSunGoal: find shade while burning.
#[derive(Clone, Debug)]
pub struct FleeSun {
@@ -2071,22 +2131,13 @@ impl Goal for FleeSun {
if !cx.w.can_see_sky(bx, by, bz) {
return false;
}
for _ in 0..10 {
let p = [
bx + m.random.next_int_bound(20).unwrap_or(0) - 10,
by + m.random.next_int_bound(6).unwrap_or(0) - 3,
bz + m.random.next_int_bound(20).unwrap_or(0) - 10,
];
if !cx.w.can_see_sky(p[0], p[1], p[2]) && m.walk_target_value(cx.w, p) < 0.0 {
self.wanted = [
f64::from(p[0]) + 0.5,
f64::from(p[1]),
f64::from(p[2]) + 0.5,
];
return true;
match hide_pos(m, cx.w) {
Some(at) => {
self.wanted = at;
true
}
None => false,
}
false
}
fn can_continue(&mut self, m: &mut Mob, _: &mut Cx) -> bool {
!m.ai.nav.is_done()
+467
View File
@@ -0,0 +1,467 @@
//! Goats (`Goat`, `GoatAi`): the lowered head (`lowerHeadTick`, entity
//! events 58 and 59) and the ram activity: `PrepareRamNearestTarget` picks
//! the nearest living thing it can see, walks to a spot four to seven
//! blocks away in a straight line and waits there 20 ticks with its head
//! down; `RamTarget` then charges at it, knocks it back, or breaks a horn
//! on a log, stone or ore. Both are one goal here.
use super::{
Damage, Mob, MobEvent, MobWorld, Who,
goals::{Cx, Goal, LOOK, MOVE, goal_clone},
};
use horizon_core::{geometry::Vec3, random::RandomSource};
/// `GoatAi.TIME_BETWEEN_RAMS` and `TIME_BETWEEN_RAMS_SCREAMER`.
const RAMS: (i32, i32) = (600, 6000);
const RAMS_SCREAMER: (i32, i32) = (100, 300);
const RAM_PREPARE_TIME: i64 = 20;
const MIN_RAM_DISTANCE: i32 = 4;
const MAX_RAM_DISTANCE: i32 = 7;
/// `PrepareRamNearestTarget.TIME_OUT_DURATION` and `RamTarget`'s.
const PREPARE_TIME_OUT: i32 = 160;
const RAM_TIME_OUT: i32 = 200;
/// `NEAREST_VISIBLE_LIVING_ENTITIES` reaches 16 blocks.
const SENSE_RANGE: f64 = 16.0;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
enum Phase {
#[default]
Done,
Prepare,
/// `RAM_TARGET` is set; `RamTarget` starts on the next tick.
RamStart,
Ram,
}
#[derive(Clone, Copy, Debug)]
struct Candidate {
start: [i32; 3],
target_pos: [i32; 3],
target: Who,
}
/// The lowered head and the ram behaviours' state. `RAM_COOLDOWN_TICKS` is a
/// brain memory (`ram_cooldown`), saved with the others.
#[derive(Clone, Debug, Default)]
pub struct State {
lower_head_tick: i32,
lowering_head: bool,
phase: Phase,
candidate: Option<Candidate>,
reached_at: Option<i32>,
/// `RAM_TARGET`, and the direction of the charge.
ram_target: Option<Vec3>,
ram_direction: Vec3,
time: i32,
}
pub fn register(m: &mut Mob) {
m.ai.goals.add(2, Ram);
}
/// `Goat.handleEntityEvent`: 58 lowers the head, 59 raises it.
pub fn entity_event(m: &mut Mob, event: u8) {
match event {
58 => m.extra.goat.lowering_head = true,
59 => m.extra.goat.lowering_head = false,
_ => {}
}
}
/// `Goat.aiStep`'s `lowerHeadTick`; a client runs it too.
pub fn tick(m: &mut Mob) {
let g = &mut m.extra.goat;
g.lower_head_tick += if g.lowering_head { 1 } else { -2 };
g.lower_head_tick = g.lower_head_tick.clamp(0, 20);
// `CountDownCooldownTicks(RAM_COOLDOWN_TICKS)`: one less each tick, erased
// once it runs out.
let cooldown = ram_cooldown(m);
if cooldown > 0 {
set_ram_cooldown(m, cooldown - 1);
}
}
/// `RAM_COOLDOWN_TICKS`: 0 when absent.
pub fn ram_cooldown(m: &Mob) -> i32 {
m.extra
.memories
.iter()
.find(|x| x.0 == RAM_COOLDOWN)
.map_or(0, |x| x.1 as i32)
}
fn set_ram_cooldown(m: &mut Mob, ticks: i32) {
let memories = &mut m.extra.memories;
match memories.iter().position(|x| x.0 == RAM_COOLDOWN) {
Some(i) if ticks > 0 => memories[i].1 = i64::from(ticks),
Some(i) => {
memories.remove(i);
}
None if ticks > 0 => memories.push((RAM_COOLDOWN, i64::from(ticks), i64::MAX)),
None => {}
}
}
const RAM_COOLDOWN: &str = "ram_cooldown";
/// `Goat.getRammingXHeadRot`.
pub fn ramming_x_head_rot(m: &Mob) -> f32 {
let max = if m.is_baby() { 52.5_f32 } else { 30.0 };
m.extra.goat.lower_head_tick as f32 / 20.0 * max * (std::f32::consts::PI / 180.0)
}
/// A server's `level.broadcastEntityEvent(goat, 58 or 59)`, sent when it
/// changes what the goat does.
fn lower_head(m: &mut Mob, events: &mut Vec<MobEvent>, lower: bool) {
if m.extra.goat.lowering_head != lower {
let event: i8 = if lower { 58 } else { 59 };
entity_event(m, event as u8);
let at = [m.position.x, m.position.y, m.position.z];
events.push(MobEvent::EntityEvent {
mob: m.id,
event,
at,
});
}
}
fn between(m: &mut Mob, (min, max): (i32, i32)) -> i32 {
m.random.next_int_between_inclusive(min, max).unwrap_or(min)
}
fn rams(m: &Mob) -> (i32, i32) {
if m.extra.screaming {
RAMS_SCREAMER
} else {
RAMS
}
}
fn block_of(p: Vec3) -> [i32; 3] {
[p.x.floor() as i32, p.y.floor() as i32, p.z.floor() as i32]
}
fn play(m: &Mob, events: &mut Vec<MobEvent>, sound: &'static str, pitch: f32) {
if !m.silent {
let at = [m.position.x, m.position.y, m.position.z];
events.push(MobEvent::Sound(sound, at, 1.0, pitch));
}
}
/// `PrepareRamNearestTarget.isWalkableBlock`.
fn walkable(m: &Mob, w: &MobWorld, p: [i32; 3]) -> bool {
m.stable_destination(w, p) && m.malus(w.path_type_static(p[0], p[1], p[2])) == 0.0
}
/// `PrepareRamNearestTarget.calculateRammingStartPosition`.
fn ramming_start(m: &mut Mob, w: &MobWorld, target: [i32; 3]) -> Option<[i32; 3]> {
if !walkable(m, w, target) {
return None;
}
// Direction.Plane.HORIZONTAL: north, east, south, west.
let mut starts = Vec::new();
for (dx, dz) in [(0, -1), (1, 0), (0, 1), (-1, 0)] {
let mut p = target;
for _ in 0..MAX_RAM_DISTANCE {
p = [p[0] + dx, p[1], p[2] + dz];
if !walkable(m, w, p) {
p = [p[0] - dx, p[1], p[2] - dz];
break;
}
}
let manhattan =
(p[0] - target[0]).abs() + (p[1] - target[1]).abs() + (p[2] - target[2]).abs();
if manhattan >= MIN_RAM_DISTANCE {
starts.push(p);
}
}
let here = block_of(m.position);
let sqr = |p: &[i32; 3]| {
(0..3)
.map(|i| i64::from(p[i] - here[i]).pow(2))
.sum::<i64>()
};
starts.sort_by_key(sqr);
starts
.into_iter()
.find(|&p| m.create_path_to(w, p, 0).is_some_and(|path| path.reached))
}
/// `PrepareRamNearestTarget.chooseRamPosition`.
fn choose(m: &mut Mob, w: &MobWorld, target: Who) {
let g = &mut m.extra.goat;
g.reached_at = None;
let Some(t) = w.target_info(target) else {
g.candidate = None;
return;
};
let target_pos = block_of(t.position);
m.extra.goat.candidate = ramming_start(m, w, target_pos).map(|start| Candidate {
start,
target_pos,
target,
});
}
/// The nearest living thing the goat sees that `RAM_TARGET_CONDITIONS`
/// allow: not a goat, a player who is not creative or a spectator.
fn nearest_target(m: &mut Mob, w: &MobWorld) -> Option<Who> {
let mut best: Option<(Who, f64)> = None;
if let Some(p) = &w.player
&& p.alive
&& !p.creative
&& !p.spectator
&& !p.allies.contains(&m.id)
&& m.distance_sqr(p.position) <= SENSE_RANGE * SENSE_RANGE
&& m.sees(w, Who::Player)
{
best = Some((Who::Player, m.distance_sqr(p.position)));
}
let id = m.id;
let mobs: Vec<(u32, f64)> = w
.nearby(m.position, SENSE_RANGE, |s| {
s.id != id && s.species != super::Species::Goat && !s.playing_dead
})
.map(|s| (s.id, m.distance_sqr(s.position)))
.collect();
for (mob, d) in mobs {
if best.is_none_or(|b| d < b.1) && m.sees(w, Who::Mob(mob)) {
best = Some((Who::Mob(mob), d));
}
}
best.map(|b| b.0)
}
/// `PrepareRamNearestTarget` and `RamTarget` of the ram activity.
#[derive(Clone, Debug)]
struct Ram;
impl Goal for Ram {
goal_clone!();
fn flags(&self) -> u8 {
MOVE | LOOK
}
fn can_use(&mut self, m: &mut Mob, _cx: &mut Cx) -> bool {
// The ram activity needs the cooldown to be over (it also waits for
// no tempting player and no breed target, which goats lack here).
ram_cooldown(m) <= 0
}
fn can_continue(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
let g = &m.extra.goat;
match g.phase {
Phase::Done => false,
Phase::Prepare => {
g.time <= PREPARE_TIME_OUT
&& g.candidate
.is_some_and(|c| cx.w.target_info(c.target).is_some_and(|t| t.alive))
}
Phase::RamStart | Phase::Ram => g.ram_target.is_some() && g.time <= RAM_TIME_OUT,
}
}
fn start(&mut self, m: &mut Mob, cx: &mut Cx) {
m.extra.goat.phase = Phase::Prepare;
m.extra.goat.time = 0;
m.extra.goat.reached_at = None;
m.extra.goat.candidate = None;
if let Some(target) = nearest_target(m, cx.w) {
choose(m, cx.w, target);
}
}
fn stop(&mut self, m: &mut Mob, cx: &mut Cx) {
match m.extra.goat.phase {
// PrepareRamNearestTarget.stop: no ram target, so the head comes
// up and the cooldown starts.
Phase::Prepare => {
lower_head(m, cx.events, false);
let wait = rams(m).0;
set_ram_cooldown(m, wait);
}
// RamTarget.finishRam (also what its timeout leaves undone).
Phase::RamStart | Phase::Ram => finish_ram(m, cx.events),
Phase::Done => {}
}
m.extra.goat.phase = Phase::Done;
m.extra.goat.candidate = None;
}
fn every_tick(&self) -> bool {
true
}
fn tick(&mut self, m: &mut Mob, cx: &mut Cx) {
m.extra.goat.time += 1;
match m.extra.goat.phase {
Phase::Prepare => prepare_tick(m, cx),
Phase::RamStart => {
// RamTarget.start: charge from here at the ram target.
let (Some(target), here) = (m.extra.goat.ram_target, block_of(m.position)) else {
return;
};
let d = Vec3::new(
f64::from(here[0]) - target.x,
0.0,
f64::from(here[2]) - target.z,
);
m.extra.goat.ram_direction = d.normalize();
m.extra.goat.phase = Phase::Ram;
m.extra.goat.time = 0;
m.nav_move_to_entity(cx.w, [target.x, target.y, target.z], 0, 3.0);
ram_tick(m, cx);
}
Phase::Ram => ram_tick(m, cx),
Phase::Done => {}
}
}
}
/// `PrepareRamNearestTarget.tick`.
fn prepare_tick(m: &mut Mob, cx: &mut Cx) {
let Some(c) = m.extra.goat.candidate else {
return;
};
let Some(t) = cx.w.target_info(c.target) else {
return;
};
m.look_at([t.position.x, t.eye_y, t.position.z]);
let now = m.age as i32;
let at = [
f64::from(c.start[0]) + 0.5,
f64::from(c.start[1]),
f64::from(c.start[2]) + 0.5,
];
if m.ai.nav.is_done() && block_of(m.position) != c.start {
m.nav_move_to_entity(cx.w, at, 0, 1.25);
}
if block_of(t.position) != c.target_pos {
// The target moved: the head comes up and a new spot is chosen.
lower_head(m, cx.events, false);
m.nav_stop();
choose(m, cx.w, c.target);
} else if block_of(m.position) == c.start {
lower_head(m, cx.events, true);
let since = *m.extra.goat.reached_at.get_or_insert(now);
if i64::from(now - since) >= RAM_PREPARE_TIME {
// getEdgeOfBlock.
let (x, z) = (
f64::from(c.target_pos[0])
+ 0.5
+ 0.5 * f64::from(sign(c.target_pos[0] - c.start[0])),
f64::from(c.target_pos[2])
+ 0.5
+ 0.5 * f64::from(sign(c.target_pos[2] - c.start[2])),
);
m.extra.goat.ram_target = Some(Vec3::new(x, f64::from(c.target_pos[1]), z));
let sound = if m.extra.screaming {
"entity.goat.screaming.prepare_ram"
} else {
"entity.goat.prepare_ram"
};
let pitch = m.voice_pitch();
play(m, cx.events, sound, pitch);
m.extra.goat.candidate = None;
m.extra.goat.phase = Phase::RamStart;
}
}
}
fn sign(v: i32) -> i32 {
v.signum()
}
/// `RamTarget.tick`.
fn ram_tick(m: &mut Mob, cx: &mut Cx) {
let impact = if m.extra.screaming {
"entity.goat.screaming.ram_impact"
} else {
"entity.goat.ram_impact"
};
let from = [m.position.x, m.position.y, m.position.z];
if let Some(target) = rammed(m, cx.w) {
// hurtServer by `noAggroMobAttack`, then the knockback.
let amount = m.attrs.attack_damage;
match target {
Who::Player => cx.events.push(MobEvent::DamagePlayer {
amount,
from,
kind: "minecraft:mob_attack_no_aggro",
}),
Who::Mob(id) => cx.events.push(MobEvent::AttackMob {
target: id,
amount,
source: Damage::mob(m.id, from),
}),
}
let level = |effect: &str| {
m.extra
.effects
.iter()
.find(|e| e.0 == effect)
.map_or(0, |e| e.2 + 1)
};
let boost = 0.25 * (level("minecraft:speed") - level("minecraft:slowness")) as f32;
let factor = (m.ai.speed * 1.65).clamp(0.2, 3.0) + boost;
let force = if m.is_baby() { 1.0 } else { 2.5 };
let d = m.extra.goat.ram_direction;
cx.events.push(MobEvent::Knockback {
target,
power: f64::from(factor * force),
x: d.x,
z: d.z,
});
finish_ram(m, cx.events);
play(m, cx.events, impact, 1.0);
} else if rammed_horn_block(m, cx.w) {
play(m, cx.events, impact, 1.0);
if drop_horn(m, cx.events) {
play(m, cx.events, "entity.goat.horn_break", 1.0);
}
finish_ram(m, cx.events);
} else if m.ai.nav.is_done() || m.extra.goat.ram_target.is_none() {
// The walk target is gone: the charge ended where it could.
finish_ram(m, cx.events);
}
}
/// The first living thing in the goat's box that `RAM_TARGET_CONDITIONS`
/// allow.
fn rammed(m: &Mob, w: &MobWorld) -> Option<Who> {
let bounds = m.bounds();
if let Some(p) = &w.player
&& p.alive
&& !p.creative
&& !p.spectator
&& !p.allies.contains(&m.id)
&& p.bounds.intersects(bounds)
{
return Some(Who::Player);
}
w.others_near(bounds)
.find(|s| {
s.id != m.id
&& s.alive
&& s.species != super::Species::Goat
&& !s.playing_dead
&& s.bounds.intersects(bounds)
})
.map(|s| Who::Mob(s.id))
}
/// `RamTarget.hasRammedHornBreakingBlock`: the block one step ahead, or the
/// one above it, snaps a horn.
fn rammed_horn_block(m: &Mob, w: &MobWorld) -> bool {
let ahead = Vec3::new(m.velocity.x, 0.0, m.velocity.z).normalize();
let at = block_of(m.position.add_vector(ahead));
let snaps =
|p: [i32; 3]| w.has_tag("minecraft:snaps_goat_horn", w.block_name(p[0], p[1], p[2]));
snaps(at) || snaps([at[0], at[1] + 1, at[2]])
}
/// `Goat.dropHorn`: one of the horns it still has comes off and drops.
fn drop_horn(m: &mut Mob, events: &mut Vec<MobEvent>) -> bool {
if m.is_baby() {
return false;
}
let [left_lost, right_lost] = m.extra.lost_horns;
if left_lost && right_lost {
return false;
}
let lose_left = if left_lost {
false
} else if right_lost {
true
} else {
m.random.next_boolean()
};
m.extra.lost_horns[usize::from(!lose_left)] = true;
events.push(MobEvent::DropItem {
item: "minecraft:goat_horn".into(),
count: 1,
at: [m.position.x, m.position.y, m.position.z],
});
true
}
/// `RamTarget.finishRam`.
fn finish_ram(m: &mut Mob, events: &mut Vec<MobEvent>) {
lower_head(m, events, false);
let wait = rams(m);
let cooldown = between(m, wait);
set_ram_cooldown(m, cooldown);
m.extra.goat.ram_target = None;
m.extra.goat.phase = Phase::Done;
}
+33 -1
View File
@@ -157,12 +157,16 @@ impl Goal for OfferFlower {
fn can_continue(&mut self, _: &mut Mob, _: &mut Cx) -> bool {
self.ticks > 0
}
fn start(&mut self, m: &mut Mob, _: &mut Cx) {
fn start(&mut self, m: &mut Mob, cx: &mut Cx) {
self.ticks = 400;
m.extra.counter = 400;
// IronGolem.offerFlower(true): entity event 11.
entity_event(m, 11, cx);
}
fn stop(&mut self, m: &mut Mob, cx: &mut Cx) {
m.extra.counter = 0;
// IronGolem.offerFlower(false): entity event 34.
entity_event(m, 34, cx);
// A copper golem the offer ran out beside takes the poppy on its
// antenna (#accepts_iron_golem_gift) when it has none.
if self.ticks == 0
@@ -198,6 +202,7 @@ impl Goal for OfferFlower {
/// the target is thrown upward.
pub fn iron_hurt_target(m: &mut Mob, who: Who, cx: &mut Cx) {
m.extra.anim_ticks = 10;
entity_event(m, 4, cx);
let attack = m.attrs.attack_damage;
let amount = if attack as i32 > 0 {
attack / 2.0 + m.random.next_int_bound(attack as i32).unwrap_or(0) as f32
@@ -224,6 +229,33 @@ pub fn iron_hurt_target(m: &mut Mob, who: Who, cx: &mut Cx) {
1.0,
));
}
/// `Level.broadcastEntityEvent` for an iron golem (guests run `client_event`).
fn entity_event(m: &Mob, event: i8, cx: &mut Cx) {
cx.events.push(MobEvent::EntityEvent {
mob: m.id,
event,
at: [m.position.x, m.position.y, m.position.z],
});
}
/// `IronGolem.handleEntityEvent` on a client: 4 the swing, 11 and 34 the
/// poppy offer starting and stopping.
pub fn client_event(m: &mut Mob, event: u8) {
match event {
4 => m.extra.anim_ticks = 10,
11 => m.extra.counter = 400,
34 => m.extra.counter = 0,
_ => {}
}
}
/// The swing and offer clocks of `IronGolem.aiStep` on a client.
pub fn client_tick(m: &mut Mob) {
if m.extra.anim_ticks > 0 {
m.extra.anim_ticks -= 1;
}
if m.extra.counter > 0 {
m.extra.counter -= 1;
}
}
/// `IronGolem.aiStep` timers and anger.
pub fn iron_ai_step(m: &mut Mob) {
if m.extra.anim_ticks > 0 {
+29 -17
View File
@@ -214,12 +214,9 @@ pub fn travel(m: &mut Mob, w: &MobWorld, input: Vec3, events: &mut Vec<MobEvent>
}
true
}
/// `Guardian.aiStep`: the client animations, air, flopping on land, the
/// beam's clock, and the spikes' thorns.
pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
if !m.alive() {
return;
}
/// `Guardian.aiStep`'s `clientSideTailAnimation` and
/// `clientSideSpikesAnimation` (the server's and a client's).
pub fn tail_and_spikes(m: &mut Mob) {
let moving = m.extra.flag2;
let in_water = m.fluids.in_water;
// Tail and spikes (clientSideTailAnimation / clientSideSpikesAnimation).
@@ -244,6 +241,16 @@ pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
} else {
a.v[SPIKES] += (1.0 - a.v[SPIKES]) * 0.06;
}
}
/// `Guardian.aiStep`: the client animations, air, flopping on land, the
/// beam's clock, and the spikes' thorns.
pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
if !m.alive() {
return;
}
let moving = m.extra.flag2;
let in_water = m.fluids.in_water;
tail_and_spikes(m);
client_tick(m);
if in_water {
m.air = 300;
@@ -276,17 +283,7 @@ pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
None => w.player.as_ref().map(|p| p.eye),
};
if let Some(look) = look {
m.anim.v[EYE_Y] = if look[1] - m.eye_y() > 0.0 { 0.0 } else { 1.0 };
let yaw = m.head_yaw.to_radians();
let pitch = m.head_pitch.to_radians();
let view = [-yaw.msin() * pitch.mcos(), yaw.mcos() * pitch.mcos()];
let d = [m.position.x - look[0], m.position.z - look[2]];
let len = (d[0] * d[0] + d[1] * d[1]).sqrt().max(1e-9);
let (dx, dz) = ((d[0] / len) as f32, (d[1] / len) as f32);
// Vec3.yRot(pi / 2) of the direction from the target.
let side = [dz, -dx];
let dot = view[0] * side[0] + view[1] * side[1];
m.anim.v[EYE_X] = dot.abs().sqrt() * 2.0 * dot.signum();
look_at(m, look);
}
// Guardian.hurtServer: a still guardian's spikes hurt a melee attacker.
if m.hurt_time == 10
@@ -317,6 +314,21 @@ pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
}
}
}
/// `GuardianModel.setupAnim`'s eye: it follows `look` (the attack target's
/// eye, else the camera's), from where the guardian faces.
pub fn look_at(m: &mut Mob, look: [f64; 3]) {
m.anim.v[EYE_Y] = if look[1] - m.eye_y() > 0.0 { 0.0 } else { 1.0 };
let yaw = m.head_yaw.to_radians();
let pitch = m.head_pitch.to_radians();
let view = [-yaw.msin() * pitch.mcos(), yaw.mcos() * pitch.mcos()];
let d = [m.position.x - look[0], m.position.z - look[2]];
let len = (d[0] * d[0] + d[1] * d[1]).sqrt().max(1e-9);
let (dx, dz) = ((d[0] / len) as f32, (d[1] / len) as f32);
// Vec3.yRot(pi / 2) of the direction from the target.
let side = [dz, -dx];
let dot = view[0] * side[0] + view[1] * side[1];
m.anim.v[EYE_X] = dot.abs().sqrt() * 2.0 * dot.signum();
}
/// `ElderGuardian.customServerAiStep`: every minute, mining fatigue III for
/// players within 50 blocks; a home within 16 blocks.
pub fn elder_server_ai_step(m: &mut Mob, cx: &mut Cx) {
@@ -165,6 +165,12 @@ impl AnimationState {
pub fn stop(&mut self) {
self.started = None;
}
/// `AnimationState.fastForward`: the animation is `ticks` further on.
pub fn fast_forward(&mut self, ticks: i32) {
if let Some(s) = &mut self.started {
*s -= ticks as f32;
}
}
pub fn animate_when(&mut self, condition: bool, age: f32) {
if condition {
self.start_if_stopped(age)
File diff suppressed because it is too large. Load diff
+19
View File
@@ -498,6 +498,12 @@ fn throw_target(m: &mut Mob, who: Who, cx: &mut Cx) {
/// `HoglinBase.hurtAndThrowTarget`.
pub fn hoglin_hurt_target(m: &mut Mob, who: Who, cx: &mut Cx) {
m.extra.anim_ticks = 10;
// Entity event 4: clients start the headbutt.
cx.events.push(MobEvent::EntityEvent {
mob: m.id,
event: 4,
at: [m.position.x, m.position.y, m.position.z],
});
let attack = m.attrs.attack_damage;
let amount = if !m.is_baby() && attack as i32 > 0 {
attack / 2.0 + m.random.next_int_bound(attack as i32).unwrap_or(0) as f32
@@ -628,6 +634,19 @@ pub fn piglin_pose(
_ => {}
}
}
/// `Hoglin.handleEntityEvent` and `Zoglin`'s event 4 on a client: the
/// headbutt, which `aiStep` then counts down.
pub fn client_event(m: &mut Mob, event: u8) {
if event == 4 {
m.extra.anim_ticks = 10;
}
}
/// The headbutt's countdown (`Hoglin.aiStep`, `Zoglin.aiStep`) on a client.
pub fn client_tick(m: &mut Mob) {
if m.extra.anim_ticks > 0 {
m.extra.anim_ticks -= 1;
}
}
/// `HoglinModel.setupAnim`.
pub fn hoglin_anim(m: &Mob, s: &State, p: &mut Poses) {
let (pos, sp) = (s.pos, s.speed);
+40 -1
View File
@@ -49,9 +49,14 @@ pub fn register(m: &mut Mob) {
pub fn immobile(m: &Mob) -> bool {
m.extra.counter > 0 || m.extra.counter2 > 0 || m.extra.anim_ticks > 0
}
/// `Ravager.doHurtTarget`: the bite animation.
/// `Ravager.doHurtTarget`: the bite animation (entity event 4 for clients).
pub fn on_attack(m: &mut Mob, events: &mut Vec<MobEvent>) {
m.extra.counter = 10;
events.push(MobEvent::EntityEvent {
mob: m.id,
event: 4,
at: [m.position.x, m.position.y, m.position.z],
});
events.push(MobEvent::Sound(
"entity.ravager.attack",
[m.position.x, m.position.y, m.position.z],
@@ -67,6 +72,12 @@ pub fn blocked(m: &mut Mob, events: &mut Vec<MobEvent>) -> bool {
}
if m.random.next_double() < 0.5 {
m.extra.counter2 = 40;
// Entity event 39: the stun starts on clients.
events.push(MobEvent::EntityEvent {
mob: m.id,
event: 39,
at: [m.position.x, m.position.y, m.position.z],
});
events.push(MobEvent::Sound(
"entity.ravager.stunned",
[m.position.x, m.position.y, m.position.z],
@@ -155,6 +166,34 @@ pub fn ai_step(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
}
}
}
/// `Ravager.handleEntityEvent` 4 and 39 on a client.
pub fn entity_event(m: &mut Mob, event: u8) {
match event {
4 => m.extra.counter = 10,
39 => m.extra.counter2 = 40,
_ => {}
}
}
/// `Ravager.aiStep`'s clocks on a client: the bite and roar run down, and
/// the end of the stun starts the roar (the sounds, particles and the roar's
/// damage are the server's).
pub fn client_tick(m: &mut Mob) {
if !m.alive() {
return;
}
if m.extra.anim_ticks > 0 {
m.extra.anim_ticks -= 1;
}
if m.extra.counter > 0 {
m.extra.counter -= 1;
}
if m.extra.counter2 > 0 {
m.extra.counter2 -= 1;
if m.extra.counter2 == 0 {
m.extra.anim_ticks = 20;
}
}
}
/// `Ravager.roar`: 6 damage to everything but illagers within 4 blocks,
/// and a strong knockback (players too, through the client).
fn roar(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
+30
View File
@@ -143,6 +143,10 @@ pub fn body(m: &Mob) -> (String, String) {
baby_or(m, "wolf#main", "wolf_baby#main"),
super::tame::texture(m),
),
S::Panda => (
baby_or(m, "panda#main", "panda_baby#main"),
super::panda::texture(m),
),
S::HappyGhast => (
baby_or(m, "happy_ghast#main", "happy_ghast_baby#main"),
if baby {
@@ -250,6 +254,20 @@ pub fn body(m: &Mob) -> (String, String) {
baby_or(m, "armadillo#main", "armadillo_baby#main"),
b(&m.kind.texture),
),
// FoxRenderer.getTextureLocation: snow variant, sleeping, baby.
S::Fox => (
baby_or(m, "fox#main", "fox_baby#main"),
format!(
"entity/fox/fox{}{}{}",
if m.extra.variant == 0 { "" } else { "_snow" },
if super::fox::flag(m, super::fox::SLEEPING) {
"_sleep"
} else {
""
},
if baby { "_baby" } else { "" },
),
),
S::Pig => {
let model = if baby {
"pig_baby#main".into()
@@ -927,7 +945,13 @@ fn flip_degrees(m: &Mob) -> f32 {
fn species_rotations(m: &Mob, alpha: f32) -> glam::Mat4 {
match m.species() {
Species::Cat => super::tame::cat_rotations(m, alpha),
Species::Panda => super::panda::rotations(m, alpha),
Species::Shulker => super::shulker::rotations(m),
// FoxRenderer.setupRotations: the body follows a pounce's pitch.
Species::Fox if super::fox::flag(m, super::fox::POUNCING | super::fox::FACEPLANTED) => {
let pitch = m.previous_head_pitch + (m.head_pitch - m.previous_head_pitch) * alpha;
glam::Mat4::from_rotation_x((-pitch).to_radians())
}
_ => glam::Mat4::IDENTITY,
}
}
@@ -1409,6 +1433,12 @@ pub fn held_item_transform(
data: &MobData,
left: bool,
) -> Option<glam::Mat4> {
// PandaHoldsItemLayer: the main hand's item in its mouth.
if m.species() == Species::Panda {
return super::panda::item_offset(m, alpha)
.filter(|_| !left)
.map(|offset| base_transform(m, alpha, camera) * offset);
}
if !super::illager::shows_item(m) {
return None;
}
+43 -5
View File
@@ -15,11 +15,16 @@
//! them in `entity_event`;
//! - run its client side animation in `client_tick`, sharing the function
//! with the server's `ai_step` where the server computes the same.
use super::{Mob, Species, breeze, camel, shulker, sniffer, warden};
use super::{
Mob, Species, allay, armadillo, axolotl, breeze, camel, copper_golem, creaking, frog, goat,
golem, guardian, piglin, ravager, shulker, sniffer, warden,
};
// `Pose.id` of the poses mobs take.
pub const STANDING: u8 = 0;
pub const LONG_JUMPING: u8 = 6;
pub const CROAKING: u8 = 8;
pub const USING_TONGUE: u8 = 9;
pub const SITTING: u8 = 10;
pub const ROARING: u8 = 11;
pub const SNIFFING: u8 = 12;
@@ -36,14 +41,16 @@ pub fn pose_id(m: &Mob) -> u8 {
Species::Warden => warden::pose_id(m),
Species::Breeze => breeze::pose_id(m),
Species::Camel | Species::CamelHusk if camel::sitting(m) => SITTING,
Species::Frog => m.extra.pose,
_ => STANDING,
}
}
/// `Entity.onSyncedDataUpdated(DATA_POSE)` on a client.
pub fn set_pose(m: &mut Mob, id: u8) {
m.extra.pose = id;
let old = std::mem::replace(&mut m.extra.pose, id);
match m.species() {
Species::Frog if old != id => frog::pose_changed(m),
Species::Warden => warden::set_pose_id(m, id),
Species::Breeze => breeze::set_pose_id(m, id),
_ => {}
@@ -60,6 +67,26 @@ pub fn client_tick(m: &mut Mob, game_time: i64) {
Species::Breeze => breeze::ai_step(m),
Species::Camel | Species::CamelHusk => camel::client_tick(m, game_time),
Species::Shulker => shulker::client_tick(m),
Species::Ravager => ravager::client_tick(m),
Species::Hoglin | Species::Zoglin => piglin::client_tick(m),
Species::IronGolem => golem::client_tick(m),
Species::Creaking => creaking::animate(m),
Species::Allay => {
m.anim.vo = m.anim.v;
allay::animate(m);
}
Species::Axolotl => axolotl::tick_animation(m),
Species::Guardian | Species::ElderGuardian => {
m.anim.vo = m.anim.v;
guardian::tail_and_spikes(m);
}
// The head spin's sound is the server's.
Species::CopperGolem if !m.no_ai => {
copper_golem::animate(m);
}
Species::Armadillo => armadillo::tick(m),
Species::Frog => frog::tick(m),
Species::Goat => goat::tick(m),
_ => {}
}
}
@@ -67,9 +94,15 @@ pub fn client_tick(m: &mut Mob, game_time: i64) {
/// `handleEntityEvent` of the mob types that start animations by an event
/// (`ClientboundEntityEventPacket` for a mob of a client).
pub fn entity_event(m: &mut Mob, event: u8) {
if m.species() == Species::Armadillo {
armadillo::entity_event(m, event);
}
if m.species() == Species::Goat {
goat::entity_event(m, event);
}
let age = m.age as f32;
if m.species() == Species::Warden {
match event {
match m.species() {
Species::Warden => match event {
// Attack: the roar ends, the swing starts.
4 => {
m.anim.states[warden::ROAR].stop();
@@ -80,6 +113,11 @@ pub fn entity_event(m: &mut Mob, event: u8) {
// The sonic boom.
62 => m.anim.states[warden::SONIC_BOOM].start(age),
_ => {}
}
},
Species::Ravager => ravager::entity_event(m, event),
Species::Hoglin | Species::Zoglin => piglin::client_event(m, event),
Species::IronGolem => golem::client_event(m, event),
Species::Creaking => creaking::entity_event(m, event),
_ => {}
}
}