mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-13 15:31:45 -10:00
Merge #2540
This commit is contained in:
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,
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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 => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user