Mobs with a body slot hold a head item and a body item at once

The body species (horses, donkeys, mules, llamas, camels, wolves, happy
ghasts, nautili, sulfur cubes) kept their body item (horse armour, a
llama's carpet, a harness, a wolf's armor, a swallowed block) in the same
field as the head item, so a helmet replaced the armour and vice versa;
vanilla holds one in each slot (#7883).

Give Extra a separate body field, route every site that read or wrote
equipment through it — the equipment packet, /item, death drops and their
chances, item breaks and sun protection, the horse's hurtArmor, the wolf's
armor interactions, the mount, harness and nautilus equip paths, the
sulfur cube swallow, the conversion carry-over, the save and the load —
and keep the saved NBT compatible: equipment.body and equipment.head keep
their vanilla keys on every species.
This commit is contained in:
JustKarkat committed 2026-11-02 19:02:51 -06:00
1 parent 31f83499b6
commit 59fe490b95
30 files changed
+359 -171

No files matched your search

@@ -275,7 +275,7 @@ fn entity_state(mob: &mut Mob, species: Species, case: &Value) {
}
}
Species::HappyGhast => {
mob.extra.head = case["bodyItem"].as_str().map(Into::into);
mob.extra.body = case["bodyItem"].as_str().map(Into::into);
if flag("isRidden") {
mob.rider = Some(Default::default());
}
@@ -164,8 +164,8 @@ pub fn main() -> horizon_client::Result<()> {
m.extra.head = item;
Some(gear::HEAD)
}
"body" if drops::head_is_body(m.species()) => {
m.extra.head = item;
"body" => {
m.extra.body = item;
None
}
"saddle" => {
@@ -1025,7 +1025,7 @@ pub fn main() -> horizon_client::Result<()> {
m.persistent = true;
}
"ridden" => {
m.extra.head = Some("minecraft:red_harness".into());
m.extra.body = Some("minecraft:red_harness".into());
m.rider = Some(horizon_client::mobs::equine::Rider::default());
}
"noai" => m.no_ai = true,
+127 -28
View File
@@ -717,12 +717,20 @@ pub fn mob_to_nbt(
if let Some(saddle) = m.saddle_item() {
equipment.push(("saddle", crate::save::item_to_nbt(&saddle)));
}
if let Some(armor) = &m.extra.head {
if let Some(armor) = &m.extra.body {
equipment.push((
"body",
Tag::compound([("id", string(armor)), ("count", Tag::Int(1))]),
));
}
// A helmet given to the mount (say by `/item ... armor.head`)
// keeps its own slot.
if let Some(helmet) = &m.extra.head {
equipment.push((
"head",
Tag::compound([("id", string(helmet)), ("count", Tag::Int(1))]),
));
}
if equipment.is_empty() {
map.remove(&"equipment".into());
} else {
@@ -1164,16 +1172,7 @@ pub fn mob_to_nbt(
for (index, (slot, item)) in [
("mainhand", &m.extra.main_hand),
("offhand", &m.extra.off_hand),
(
// A happy ghast wears its harness in the body slot (and a
// nautilus its armour, a sulfur cube its swallowed block).
if crate::mobs::species::body_slot(m.species()) {
"body"
} else {
"head"
},
&m.extra.head,
),
("head", &m.extra.head),
("chest", &m.extra.chest),
("legs", &m.extra.legs),
("feet", &m.extra.feet),
@@ -1221,17 +1220,6 @@ pub fn mob_to_nbt(
equipment.insert(slot.into(), crate::save::item_to_nbt(&stack));
continue;
}
// A wolf's armor keeps the durability it lost.
if slot == "body"
&& m.species() == crate::mobs::Species::Wolf
&& let Some(id) = item
{
let mut stack = crate::items::ItemStack::new(id, 1);
stack.damage = m.extra.body_damage.max(0) as u32;
stack.extra = m.extra.body_extra.clone();
equipment.insert(slot.into(), crate::save::item_to_nbt(&stack));
continue;
}
match item {
Some(id) => {
let keep = slot != "mainhand"
@@ -1269,6 +1257,19 @@ pub fn mob_to_nbt(
}
}
}
// A happy ghast wears its harness in the body slot (and a nautilus
// its armour, a sulfur cube its swallowed block, a wolf its armor,
// with the durability it lost).
if crate::mobs::species::body_slot(m.species())
&& let Some(id) = &m.extra.body
{
let mut stack = crate::items::ItemStack::new(id, 1);
if m.species() == crate::mobs::Species::Wolf {
stack.damage = m.extra.body_damage.max(0) as u32;
stack.extra = m.extra.body_extra.clone();
}
equipment.insert("body".into(), crate::save::item_to_nbt(&stack));
}
// Vanilla saves `equipment` only when something is equipped.
if equipment.is_empty() {
map.remove(&"equipment".into());
@@ -1478,11 +1479,12 @@ pub fn mob_from_nbt(
.map(Into::into);
if !crate::mobs::equine::is_equine(m.species()) {
let slot = |key| eq.get(key).and_then(|i| text(i.get("id"))).map(Into::into);
m.extra.head = slot(if crate::mobs::species::body_slot(m.species()) {
"body"
} else {
"head"
});
m.extra.head = slot("head");
// A happy ghast wears its harness in the body slot (and a
// nautilus its armour, a sulfur cube its swallowed block).
if crate::mobs::species::body_slot(m.species()) {
m.extra.body = slot("body");
}
m.extra.chest = slot("chest");
m.extra.legs = slot("legs");
m.extra.feet = slot("feet");
@@ -1655,10 +1657,14 @@ pub fn mob_from_nbt(
eq.and_then(|e| e.get("saddle"))
.and_then(crate::save::item_from_nbt),
);
m.extra.head = eq
m.extra.body = eq
.and_then(|e| e.get("body"))
.and_then(|b| text(b.get("id")))
.map(Into::into);
m.extra.head = eq
.and_then(|e| e.get("head"))
.and_then(|b| text(b.get("id")))
.map(Into::into);
if let Some(Tag::List(attrs)) = tag.get("attributes") {
for a in attrs {
let (Some(id), Some(Tag::Double(bits))) = (text(a.get("id")), a.get("base")) else {
@@ -3423,6 +3429,99 @@ mod tests {
use super::*;
use crate::items::ItemStack;
/// A mount keeps its body armour and a helmet (say
/// `/item replace entity <horse> armor.head with minecraft:iron_helmet`)
/// in their own slots through a save: vanilla holds both (`equipment.body`
/// and `equipment.head`), one in each.
#[test]
fn a_horses_save_keeps_a_helmet_and_its_body_armour_apart() {
let mut kind = crate::mobs::sensor_scan_tests::kind();
let ty = Arc::make_mut(&mut kind);
ty.id = "minecraft:horse".into();
ty.species = crate::mobs::Species::Horse;
let types = [("minecraft:horse".into(), kind)].into();
let raw = Tag::compound([
("id", string("minecraft:horse")),
("Pos", doubles(&[4.5, 220.0, 4.5])),
("Tame", Tag::Byte(1)),
(
"equipment",
Tag::compound([
(
"body",
Tag::compound([
("id", string("minecraft:diamond_horse_armor")),
("count", Tag::Int(1)),
]),
),
(
"head",
Tag::compound([
("id", string("minecraft:iron_helmet")),
("count", Tag::Int(1)),
]),
),
]),
),
]);
let m = mob_from_nbt(&raw, &types, 1, None).unwrap();
assert_eq!(
m.extra.body.as_deref(),
Some("minecraft:diamond_horse_armor")
);
assert_eq!(m.extra.head.as_deref(), Some("minecraft:iron_helmet"));
let saved = mob_to_nbt(&m, [0; 4], Some(&raw), None);
let eq = saved.get("equipment").unwrap();
let id = |key: &str| {
eq.get(key)
.and_then(|i| text(i.get("id")))
.unwrap_or_default()
};
assert_eq!(id("body"), "minecraft:diamond_horse_armor");
assert_eq!(id("head"), "minecraft:iron_helmet");
}
/// A wolf loads its armor (`equipment.body`) into the body slot with the
/// durability it lost, and a helmet next to it into the head slot.
#[test]
fn a_wolfs_save_keeps_its_armour_and_a_helmet_apart() {
let mut kind = crate::mobs::sensor_scan_tests::kind();
let ty = Arc::make_mut(&mut kind);
ty.id = "minecraft:wolf".into();
ty.species = crate::mobs::Species::Wolf;
let types = [("minecraft:wolf".into(), kind)].into();
let armor = Tag::compound([
("id", string("minecraft:wolf_armor")),
("count", Tag::Int(1)),
(
"components",
Tag::compound([("minecraft:damage", Tag::Int(9))]),
),
]);
let raw = Tag::compound([
("id", string("minecraft:wolf")),
("Pos", doubles(&[4.5, 220.0, 4.5])),
("Owner", Tag::IntArray([0; 4].to_vec())),
(
"equipment",
Tag::compound([
("body", armor),
(
"head",
Tag::compound([
("id", string("minecraft:iron_helmet")),
("count", Tag::Int(1)),
]),
),
]),
),
]);
let m = mob_from_nbt(&raw, &types, 1, None).unwrap();
assert_eq!(m.extra.body.as_deref(), Some("minecraft:wolf_armor"));
assert_eq!(m.extra.body_damage, 9);
assert_eq!(m.extra.head.as_deref(), Some("minecraft:iron_helmet"));
}
fn fused_creeper(fuse: Option<i16>) -> (Mob, Tag) {
let mut kind = crate::mobs::sensor_scan_tests::kind();
let ty = Arc::make_mut(&mut kind);
+12 -16
View File
@@ -532,22 +532,18 @@ impl Game<'_> {
i.as_ref().map(|id| item_view(&ItemStack::new(id, 1)))
};
let mut equipment: [Option<ItemView>; 7] = Default::default();
if !horizon_client::mobs::equine::is_equine(m.species()) {
equipment[0] = slot(&m.extra.main_hand);
equipment[1] = slot(&m.extra.off_hand);
equipment[2] = slot(&m.extra.head);
equipment[3] = slot(&m.extra.chest);
equipment[4] = slot(&m.extra.legs);
equipment[5] = slot(&m.extra.feet);
}
// A wolf's armor is its body slot, with its durability.
if m.species() == horizon_client::mobs::Species::Wolf {
equipment[6] = m.extra.head.as_ref().map(|id| {
let mut armor = ItemStack::new(id, 1);
armor.damage = m.extra.body_damage.max(0) as u32;
item_view(&armor)
});
}
equipment[0] = slot(&m.extra.main_hand);
equipment[1] = slot(&m.extra.off_hand);
equipment[2] = slot(&m.extra.head);
equipment[3] = slot(&m.extra.chest);
equipment[4] = slot(&m.extra.legs);
equipment[5] = slot(&m.extra.feet);
// The body slot (a wolf's armor, a horse's), with its durability.
equipment[6] = m.extra.body.as_ref().map(|id| {
let mut armor = ItemStack::new(id, 1);
armor.damage = m.extra.body_damage.max(0) as u32;
item_view(&armor)
});
EntityView {
kind: m.kind.id.clone(),
position: [m.position.x, m.position.y, m.position.z],
@@ -202,7 +202,8 @@ impl Game<'_> {
let item = match *key {
"mainhand" => &m.extra.main_hand,
"offhand" => &m.extra.off_hand,
"head" | "body" => &m.extra.head,
"head" => &m.extra.head,
"body" => &m.extra.body,
"chest" => &m.extra.chest,
"legs" => &m.extra.legs,
"feet" => &m.extra.feet,
@@ -285,7 +286,8 @@ impl Game<'_> {
match *key {
"mainhand" => m.extra.set_main_hand_stack(stack),
"offhand" => m.extra.off_hand = item,
"head" | "body" => m.extra.head = item,
"head" => m.extra.head = item,
"body" => m.extra.body = item,
"chest" => m.extra.chest = item,
"legs" => m.extra.legs = item,
"feet" => m.extra.feet = item,
+9 -8
View File
@@ -464,7 +464,7 @@ impl Game<'_> {
let Some(m) = self.mobs.iter().find(|m| m.id == id) else {
return;
};
let body = horizon_client::mobs::drops::head_is_body(m.species());
let body = horizon_client::mobs::drops::body_slot_species(m.species());
let slot = if body {
EquipmentSlot::Body
} else {
@@ -501,7 +501,7 @@ impl Game<'_> {
match (body, broke) {
(true, false) => m.extra.body_damage = stack.damage as i32,
(true, true) => {
m.extra.head = None;
m.extra.body = None;
m.extra.body_damage = 0;
}
(false, false) => m.extra.armor[0] = Some(stack),
@@ -580,7 +580,7 @@ impl Game<'_> {
return;
};
if lost >= max {
m.extra.head = None;
m.extra.body = None;
m.extra.body_damage = 0;
true
} else {
@@ -727,6 +727,7 @@ impl Game<'_> {
"temper": m.extra.variant3,
"chested": m.extra.chested,
"head": m.extra.head.as_deref(),
"body": m.extra.body.as_deref(),
"warden": horizon_client::mobs::warden::describe(m),
"villager": horizon_client::mobs::villager::ext(m).map(|e| serde_json::json!({
"food_level": e.food_level,
@@ -5425,14 +5426,14 @@ impl Game<'_> {
if item == "minecraft:shears"
&& m.extra.tame
&& m.extra.owner == Some(acting)
&& m.extra.head.is_some()
&& m.extra.body.is_some()
&& !sneaking =>
{
let mut stack = ItemStack::new("minecraft:wolf_armor", 1);
stack.damage = m.extra.body_damage.max(0) as u32;
stack.extra = m.extra.body_extra.take();
armor_drop = Some(stack);
m.extra.head = None;
m.extra.body = None;
m.extra.body_damage = 0;
eat = Some(("item.armor.unequip_wolf", 1.0, 1.0));
self.damage_held_tool();
@@ -5456,10 +5457,10 @@ impl Game<'_> {
if item == "minecraft:wolf_armor"
&& m.extra.tame
&& m.extra.owner == Some(acting)
&& m.extra.head.is_none()
&& m.extra.body.is_none()
&& !m.is_baby() =>
{
m.extra.head = Some(item.as_str().into());
m.extra.body = Some(item.as_str().into());
m.extra.body_damage = held_damage;
m.extra.body_extra = held_extra;
consume = true;
@@ -5472,7 +5473,7 @@ impl Game<'_> {
&& m.extra.tame
&& m.extra.owner == Some(acting)
&& m.extra.sitting
&& m.extra.head.is_some()
&& m.extra.body.is_some()
&& m.extra.body_damage > 0 =>
{
m.extra.body_damage = (m.extra.body_damage
@@ -5,7 +5,7 @@
//! changed in the format of `VanillaDispenserOracle`.
use super::{
Game,
dispenser::{Slot, body_in_head, mob_set_slot, mob_slot_item},
dispenser::{Slot, mob_set_slot, mob_slot_item},
};
use horizon_client::{
Result,
@@ -936,6 +936,7 @@ impl Game<'_> {
&mut mob.extra.main_hand,
&mut mob.extra.off_hand,
&mut mob.extra.head,
&mut mob.extra.body,
&mut mob.extra.chest,
&mut mob.extra.legs,
&mut mob.extra.feet,
@@ -951,8 +952,8 @@ impl Game<'_> {
if flag("baby") && ageable {
mob.extra.age = -24000;
}
let tamable =
body_in_head(s) || matches!(s, Species::Cat | Species::Parrot | Species::Ocelot);
let tamable = horizon_client::mobs::drops::body_slot_species(s)
|| matches!(s, Species::Cat | Species::Parrot | Species::Ocelot);
if flag("tame") && tamable {
mob.extra.tame = true;
}
+9 -14
View File
@@ -228,24 +228,17 @@ fn equine(s: Species) -> bool {
| Species::TraderLlama
)
}
/// Mobs whose body slot Horizon keeps in `extra.head` (no helmet slot is drawn).
pub(super) fn body_in_head(s: Species) -> bool {
equine(s)
|| matches!(s, Species::Wolf | Species::HappyGhast | Species::SulfurCube)
|| horizon_client::mobs::nautilus::is_nautilus(s)
}
/// What a mob holds in a slot.
pub(super) fn mob_slot_item(m: &Mob, slot: Slot) -> Option<Arc<str>> {
let x = &m.extra;
let body = body_in_head(m.species());
match slot {
Slot::MainHand => x.main_hand.clone(),
Slot::OffHand => x.off_hand.clone(),
Slot::Feet => x.feet.clone(),
Slot::Legs => x.legs.clone(),
Slot::Chest => x.chest.clone(),
Slot::Head => x.head.clone().filter(|_| !body),
Slot::Body => x.head.clone().filter(|_| body),
Slot::Head => x.head.clone(),
Slot::Body => x.body.clone(),
Slot::Saddle => m.saddle_item().map(|s| s.item),
}
}
@@ -317,7 +310,8 @@ pub(super) fn mob_set_slot(m: &mut Mob, slot: Slot, stack: ItemStack) {
Slot::Feet => x.feet = Some(item),
Slot::Legs => x.legs = Some(item),
Slot::Chest => x.chest = Some(item),
Slot::Head | Slot::Body => x.head = Some(item),
Slot::Head => x.head = Some(item),
Slot::Body => x.body = Some(item),
Slot::Saddle => m.set_saddle(Some(stack)),
}
}
@@ -1835,6 +1829,7 @@ impl Game<'_> {
&mut mob.extra.main_hand,
&mut mob.extra.off_hand,
&mut mob.extra.head,
&mut mob.extra.body,
&mut mob.extra.chest,
&mut mob.extra.legs,
&mut mob.extra.feet,
@@ -1871,7 +1866,7 @@ impl Game<'_> {
// passenger attachment, and nothing is swallowed for 5 s.
let height = f64::from(m.height());
let id = m.id;
if let Some(body) = self.mobs[i].extra.head.take() {
if let Some(body) = self.mobs[i].extra.body.take() {
self.spawn_at_location(at, height, ItemStack::new(&body, 1));
}
// setItemSlot(BODY, EMPTY): an unequip.
@@ -2114,16 +2109,16 @@ impl Game<'_> {
.collect();
for i in cubes {
let m = &self.mobs[i];
if m.is_baby() || m.extra.head.as_deref() == Some(&*stack.item) {
if m.is_baby() || m.extra.body.as_deref() == Some(&*stack.item) {
continue;
}
let (at, height) = (m.position, f64::from(m.height()));
// The swallowed block comes out on top (the passenger attachment).
if let Some(old) = self.mobs[i].extra.head.take() {
if let Some(old) = self.mobs[i].extra.body.take() {
self.spawn_at_location(at, height, ItemStack::new(&old, 1));
}
let m = &mut self.mobs[i];
m.extra.head = Some(stack.item.clone());
m.extra.body = Some(stack.item.clone());
m.persistent = true;
let (at, id) = (m.position, m.id);
// setItemSlot: a body item without `equippable` posts UNEQUIP.
+3 -3
View File
@@ -66,10 +66,10 @@ impl Game<'_> {
}
let at = m.center();
let sneaking = self.keys.contains(&winit::keyboard::KeyCode::ShiftLeft);
let harnessed = m.extra.head.is_some();
let harnessed = m.extra.body.is_some();
// Equippable.equipOnInteract: a harness goes on a bare adult.
if item.ends_with("_harness") && item.starts_with("minecraft:") && !harnessed {
self.mobs[index].extra.head = Some(item.into());
self.mobs[index].extra.body = Some(item.into());
self.audio
.play("minecraft:entity.happy_ghast.equip", Some(at), 1.0, 1.0);
self.consume_held();
@@ -79,7 +79,7 @@ impl Game<'_> {
if item == "minecraft:shears"
&& harnessed
&& !sneaking
&& let Some(harness) = self.mobs[index].extra.head.take()
&& let Some(harness) = self.mobs[index].extra.body.take()
{
self.audio
.play("minecraft:entity.happy_ghast.unequip", Some(at), 1.0, 1.0);
+4 -4
View File
@@ -110,18 +110,18 @@ impl Game<'_> {
let m = &self.mobs[index];
let carpet = |i: &str| i.ends_with("_carpet") && i.starts_with("minecraft:");
if !item.is_empty() {
if item == "minecraft:shears" && m.extra.head.as_deref().is_some_and(carpet) {
if item == "minecraft:shears" && m.extra.body.as_deref().is_some_and(carpet) {
// Equippable.canBeSheared: the carpet comes off.
let at = m.center();
let carpet = self.mobs[index].extra.head.take().unwrap_or_default();
let carpet = self.mobs[index].extra.body.take().unwrap_or_default();
self.audio
.play("minecraft:item.llama_carpet.unequip", Some(at), 1.0, 1.0);
self.spawn_item(ItemStack::new(&carpet, 1), at, [0.0, 0.2, 0.0], 10);
self.damage_held_tool();
return true;
}
if carpet(item) && m.extra.head.is_none() {
self.mobs[index].extra.head = Some(item.into());
if carpet(item) && m.extra.body.is_none() {
self.mobs[index].extra.body = Some(item.into());
self.llama_sound(index, "entity.llama.swag", 1.0);
self.consume_held();
return true;
+3 -3
View File
@@ -252,7 +252,7 @@ impl Game<'_> {
let m = &self.mobs[index];
let species = m.species();
let body =
shows_body(species).then(|| m.extra.head.as_deref().map(|i| ItemStack::new(i, 1)));
shows_body(species).then(|| m.extra.body.as_deref().map(|i| ItemStack::new(i, 1)));
let mut items = m.extra.items.clone();
items.resize(mount_columns(m) * 3, None);
(m.saddle_item(), body, items)
@@ -274,7 +274,7 @@ impl Game<'_> {
} else {
body.as_ref()
.and_then(Option::as_ref)
.filter(|_| m.extra.head.is_none())
.filter(|_| m.extra.body.is_none())
};
let sound = if saddle_put_on && nautilus::is_nautilus(m.species()) {
// AbstractNautilus.getEquipSound: its own saddle sounds.
@@ -291,7 +291,7 @@ impl Game<'_> {
let m = &mut self.mobs[index];
m.set_saddle(saddle);
if let Some(body) = body {
m.extra.head = body.map(|a| a.item);
m.extra.body = body.map(|a| a.item);
}
let mut items = items;
let keep = m.extra.items.len().max(CHEST_SIZE);
+2 -2
View File
@@ -243,9 +243,9 @@ impl Game<'_> {
} else if item.ends_with("_horse_armor")
&& species == Species::Horse
&& tamed
&& m.extra.head.is_none()
&& m.extra.body.is_none()
{
m.extra.head = Some(item.into());
m.extra.body = Some(item.into());
Some("minecraft:entity.horse.armor")
} else if item == "minecraft:chest"
&& matches!(species, Species::Donkey | Species::Mule)
+2 -2
View File
@@ -117,8 +117,8 @@ impl Game<'_> {
self.consume_held();
return true;
}
if item.ends_with("_nautilus_armor") && tame && m.extra.head.is_none() {
m.extra.head = Some(item.into());
if item.ends_with("_nautilus_armor") && tame && m.extra.body.is_none() {
m.extra.body = Some(item.into());
self.nautilus_sound(index, "item.armor.equip_nautilus");
self.consume_held();
return true;
+3 -6
View File
@@ -3732,9 +3732,6 @@ impl Game<'_> {
{
m.extra.charged = charged;
}
// The body slot (6: horse armour, llama carpet, harness)
// is kept in `head`, as Horizon's own mobs do.
let body = horizon_client::mobs::drops::head_is_body(m.species());
for (slot, item, stack) in items {
let e = &mut m.extra;
match slot {
@@ -3759,14 +3756,14 @@ impl Game<'_> {
e.chest = item;
e.armor[1] = stack;
}
5 if !body => {
5 => {
e.head = item;
e.armor[0] = stack;
}
6 if body => {
6 => {
// A wolf's armor shows its cracks.
e.body_damage = stack.as_ref().map_or(0, |s| s.damage as i32);
e.head = item;
e.body = item;
}
7 => m.set_saddle(stack),
_ => {}
@@ -53,7 +53,7 @@ impl Game<'_> {
// Pig / Strider.mobInteract: a saddled one is ridden.
Species::Pig | Species::Strider => m.extra.saddled && !sneaking,
// HappyGhast.mobInteract: a harnessed adult is ridden.
Species::HappyGhast => !baby && m.extra.head.is_some(),
Species::HappyGhast => !baby && m.extra.body.is_some(),
// TamableAnimal (wolf, cat, parrot) on a client: CONSUME once tame.
Species::Wolf | Species::Cat | Species::Parrot => m.extra.tame,
// Allay.mobInteract: gives an item or takes it back.
@@ -561,7 +561,7 @@ mod tests {
return;
};
assert!(!client_moves(&m, true), "ghast without a harness");
m.extra.head = Some("minecraft:white_happy_ghast_harness".into());
m.extra.body = Some("minecraft:white_happy_ghast_harness".into());
assert!(client_moves(&m, true), "harnessed adult");
assert!(!client_moves(&m, false), "behind another rider");
m.extra.age = -1;
@@ -45,7 +45,7 @@ impl Game<'_> {
light: u32,
out: &mut Vec<Vertex>,
) {
let Some(item) = mob.extra.head.as_deref() else {
let Some(item) = mob.extra.body.as_deref() else {
return;
};
if super::armor_stands::skull_kind(item).is_some() {
@@ -85,7 +85,7 @@ impl Game<'_> {
/// Spit the swallowed block out on top (`EntityAttachment.PASSENGER`).
fn eject_sulfur_block(&mut self, index: usize) {
let m = &mut self.mobs[index];
let Some(item) = m.extra.head.take() else {
let Some(item) = m.extra.body.take() else {
return;
};
let at = [
@@ -133,7 +133,7 @@ impl Game<'_> {
}
if item == "minecraft:shears" && sulfur_cube::has_body(m) {
// shear: out comes the block, and nothing is swallowed for 5 s.
let sheared = self.mobs[index].extra.head.clone();
let sheared = self.mobs[index].extra.body.clone();
self.eject_sulfur_block(index);
self.mobs[index].extra.ints[3] = sulfur_cube::PICKUP_TIMER;
self.sulfur_sound(index, "entity.sulfur_cube.eject");
@@ -151,11 +151,11 @@ impl Game<'_> {
}
if sulfur_cube::swallowable(m, item) {
// equipItem: not the same block again; the old one comes out.
if m.extra.head.as_deref() == Some(item) {
if m.extra.body.as_deref() == Some(item) {
return false;
}
self.eject_sulfur_block(index);
self.mobs[index].extra.head = Some(item.into());
self.mobs[index].extra.body = Some(item.into());
self.consume_held();
self.sulfur_sound(index, "entity.sulfur_cube.absorb");
return true;
@@ -264,7 +264,7 @@ impl Game<'_> {
self.remove_dropped(j);
}
let m = &mut self.mobs[i];
m.extra.head = Some(item);
m.extra.body = Some(item);
m.extra.point = None;
self.sulfur_sound(i, "entity.sulfur_cube.absorb");
if thrown_by_player {
+1
View File
@@ -1,4 +1,5 @@
//! Gameplay and startup, above the reusable client library.
#![recursion_limit = "256"]
#![forbid(unsafe_code)]
extern crate self as horizon_client;
pub use horizon_client_lib::*;
+57 -23
View File
@@ -892,7 +892,7 @@ pub struct Extra {
/// `Mob.canPickUpLoot` (also lets dispensers equip it).
pub pick_up_loot: bool,
pub saddled: bool,
/// The durability a wolf's body armor (`extra.head`) has lost.
/// The durability a wolf's body armor (`extra.body`) has lost.
pub body_damage: i32,
/// The components (dye, name...) of that armor stack.
pub body_extra: Option<Box<crate::item_data::StackData>>,
@@ -936,7 +936,11 @@ pub struct Extra {
/// Components of the current held stack; setters clear these on replacement.
main_hand_stack: Option<crate::items::ItemStack>,
pub off_hand: Option<Arc<str>>,
/// The head item (`getItemBySlot(HEAD)`), on every mob.
pub head: Option<Arc<str>>,
/// The body item (`getItemBySlot(BODY)`): a horse's armour, a llama's
/// carpet, a wolf's armour, a harness, a sulfur cube's swallowed block.
pub body: Option<Arc<str>>,
/// Worn armour below the head (humanoid mobs).
pub chest: Option<Arc<str>>,
pub legs: Option<Arc<str>>,
@@ -2112,9 +2116,6 @@ impl Mob {
pub fn equipment_stacks(&self) -> [Option<crate::items::ItemStack>; 8] {
use crate::items::ItemStack;
let e = &self.extra;
// A horse's armour, a llama's carpet, a harness: Horizon keeps the body
// slot's item (6) in `head`.
let body = drops::head_is_body(self.species());
let wolf = self.species() == Species::Wolf;
let slots = [
(0usize, &e.main_hand, e.main_hand_stack()),
@@ -2122,12 +2123,10 @@ impl Mob {
(2, &e.feet, e.armor[3].as_ref()),
(3, &e.legs, e.armor[2].as_ref()),
(4, &e.chest, e.armor[1].as_ref()),
// The body slot's item is no worn armour.
(
if body { 6 } else { 5 },
&e.head,
e.armor[0].as_ref().filter(|_| !body),
),
// The body slot's item is no worn armour: a horse's armour, a
// llama's carpet, a harness go out on their own (6).
(5usize, &e.head, e.armor[0].as_ref()),
(6, &e.body, None),
];
let mut out: [Option<ItemStack>; 8] = Default::default();
for (slot, item, worn) in slots {
@@ -2784,12 +2783,12 @@ impl Mob {
/// the last-hurt-by bookkeeping goals read.
/// The `armor` and `armor_toughness` modifiers of worn humanoid armour.
pub fn worn_armor(&self) -> (f32, f32) {
// An equine keeps its horse armor in `extra.head`: the item's `armor`
// An equine keeps its horse armor in `extra.body`: the item's `armor`
// and `armor_toughness` modifiers of the body slot.
if equine::is_equine(self.species()) {
return self
.extra
.head
.body
.as_deref()
.map_or((0.0, 0.0), spawning::horse_armor_stats);
}
@@ -3003,11 +3002,10 @@ impl Mob {
// it hands `doHurtEquipment` the body slot alone. A wolf's armour has
// its own rule (`Wolf.actuallyHurt`, `hurt_wolf_armor`) and a player's
// four pieces theirs, so the other "body slot" species — donkey, mule,
// llama, camel, the skeleton and zombie horses — wear nothing, whatever
// `drops::head_is_body` says about where the item lives.
// llama, camel, the skeleton and zombie horses — wear nothing.
if let Some(damage) = armor_hit
&& self.species() == Species::Horse
&& self.extra.head.is_some()
&& self.extra.body.is_some()
{
self.outbox.push(MobEvent::ArmorHurt {
mob: self.id,
@@ -3110,7 +3108,7 @@ impl Mob {
/// the world and the untyped kills).
fn wolf_armor_absorbs(&self, source: &Damage) -> bool {
self.species() == Species::Wolf
&& self.extra.head.as_deref() == Some("minecraft:wolf_armor")
&& self.extra.body.as_deref() == Some("minecraft:wolf_armor")
&& !matches!(
source.kind,
DamageKind::Drown
@@ -3138,7 +3136,7 @@ impl Mob {
let after = before + damage.ceil() as i32;
let at = [self.position.x, self.position.y, self.position.z];
if after >= WOLF_ARMOR_DURABILITY {
self.extra.head = None;
self.extra.body = None;
self.extra.body_damage = 0;
self.extra.body_extra = None;
self.outbox
@@ -4483,7 +4481,7 @@ mod armor_hurt_tests {
from: None,
};
let mut m = horse();
m.extra.head = Some("minecraft:diamond_horse_armor".into());
m.extra.body = Some("minecraft:diamond_horse_armor".into());
assert!(m.damage(6.0, hit), "the hit landed");
let owed = m.outbox.iter().find_map(|e| match e {
MobEvent::ArmorHurt { amount, .. } => Some(*amount),
@@ -4508,12 +4506,11 @@ mod armor_hurt_tests {
/// base one is empty, `LivingEntity.java:1887`), so a donkey wearing a body
/// item — `/item replace entity @e[type=minecraft:donkey,limit=1]
/// armor.body with minecraft:wolf_armor` — wears nothing, even though
/// `drops::head_is_body` stores a donkey's body item in the slot a horse's
/// goes in.
/// vanilla only gives a horse's body piece the wear.
#[test]
fn a_donkey_wearing_a_body_item_wears_nothing() {
let mut donkey = mob("minecraft:donkey", Species::Donkey);
donkey.extra.head = Some("minecraft:wolf_armor".into());
donkey.extra.body = Some("minecraft:wolf_armor".into());
assert!(
donkey.damage(6.0, Damage::of(DamageKind::MobAttack)),
"the hit landed"
@@ -4527,13 +4524,50 @@ mod armor_hurt_tests {
);
}
/// Vanilla holds a head item and a body item at once on the mobs with a
/// body slot (`LivingEntity.setItemSlot` over `EntityEquipment`): the
/// packet keeps the helmet on 5 and the armour on 6, `/item` reads each
/// back, and the armour value is the body piece's.
#[test]
fn a_horse_holds_a_helmet_and_its_body_armour_at_once() {
use horizon_world::player_inventory::EquipmentSlot;
let mut m = horse();
m.extra.head = Some("minecraft:iron_helmet".into());
m.extra.body = Some("minecraft:diamond_horse_armor".into());
let gear = m.equipment_stacks();
assert_eq!(
gear[5].as_ref().map(|s| s.item.as_ref().to_owned()),
Some("minecraft:iron_helmet".to_owned())
);
assert_eq!(
gear[6].as_ref().map(|s| s.item.as_ref().to_owned()),
Some("minecraft:diamond_horse_armor".to_owned())
);
let slot = |s| m.item_by_slot(s).map(|i| i.item.as_ref().to_owned());
assert_eq!(
slot(EquipmentSlot::Head),
Some("minecraft:iron_helmet".to_owned())
);
assert_eq!(
slot(EquipmentSlot::Body),
Some("minecraft:diamond_horse_armor".to_owned())
);
let (armor, _) = m.worn_armor();
assert!(armor > 0.0, "the body piece armors the horse");
// Death keeps both (`Mob.dropCustomDeathLoot` by slot).
let body = crate::mobs::drops::slot_item(&m, "body").unwrap();
let head = crate::mobs::drops::slot_item(&m, "head").unwrap();
assert_eq!(body.item, "minecraft:diamond_horse_armor");
assert_eq!(head.item, "minecraft:iron_helmet");
}
/// `getDamageAfterArmorAbsorb` asks `hurtArmor` only when the damage type
/// is not in `#bypasses_armor` (`LivingEntity.java:1907-1909`), so a horse
/// that falls keeps its body piece whole.
#[test]
fn a_fall_does_not_wear_the_body_piece() {
let mut m = horse();
m.extra.head = Some("minecraft:diamond_horse_armor".into());
m.extra.body = Some("minecraft:diamond_horse_armor".into());
assert!(
m.damage(6.0, Damage::of(DamageKind::Fall)),
"the fall landed"
@@ -4555,7 +4589,7 @@ mod armor_hurt_tests {
let mut m = horse();
let k = Arc::make_mut(&mut m.kind);
k.armor = 20.0;
m.extra.head = Some("minecraft:diamond_horse_armor".into());
m.extra.body = Some("minecraft:diamond_horse_armor".into());
let before = m.health;
assert!(m.damage(8.0, Damage::of(DamageKind::MobAttack)));
assert!(m.health < before, "the hit reached the health");
+20 -6
View File
@@ -1140,10 +1140,17 @@ fn can_freeze(m: &Mob, w: &MobWorld) -> bool {
let immune = w
.items
.and_then(|i| i.item_tags.get("minecraft:freeze_immune_wearables"));
![&m.extra.head, &m.extra.chest, &m.extra.legs, &m.extra.feet]
.into_iter()
.flatten()
.any(|item| immune.is_some_and(|t| t.contains(&**item)))
// A wolf's armor (its body slot) is `#freeze_immune_wearables`.
![
&m.extra.body,
&m.extra.head,
&m.extra.chest,
&m.extra.legs,
&m.extra.feet,
]
.into_iter()
.flatten()
.any(|item| immune.is_some_and(|t| t.contains(&**item)))
}
/// `Mob.playAmbientSound` on demand: a spawn egg or a mob bucket just placed `m`.
pub fn ambient_sound(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
@@ -1560,7 +1567,14 @@ fn push_entities(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
/// or without AI (`NoAI` only skips `serverAiStep`).
fn burn_undead(m: &mut Mob, w: &MobWorld, events: &mut Vec<MobEvent>) {
if m.kind.flags.burns_in_day && m.alive() && sun_burn_tick(m, w) {
if m.extra.head.is_some() {
// The head is the sun-protection slot, or the body for the species
// whose body item is their armour (a zombie horse's, a nautilus's).
let protected = if super::drops::body_slot_species(m.species()) {
m.extra.body.is_some()
} else {
m.extra.head.is_some()
};
if protected {
events.push(MobEvent::SunProtectionWear { mob: m.id });
} else {
m.ignite_for_ticks(160);
@@ -3149,7 +3163,7 @@ mod same_vehicle_push_tests {
);
assert_eq!(worn.iter().flatten().count(), 2);
let mut horse = mob("minecraft:horse");
horse.extra.head = Some(std::sync::Arc::from("minecraft:iron_horse_armor"));
horse.extra.body = Some(std::sync::Arc::from("minecraft:iron_horse_armor"));
horse.set_saddle(Some(crate::items::ItemStack::new("minecraft:saddle", 1)));
let gear = horse.equipment_stacks();
assert_eq!(
+1 -1
View File
@@ -936,7 +936,7 @@ mod tests {
0.0,
);
mob.extra.tame = true;
mob.extra.head = Some("minecraft:wolf_armor".into());
mob.extra.body = Some("minecraft:wolf_armor".into());
// Worn through: the high cracks.
mob.extra.body_damage = WOLF_ARMOR_DURABILITY;
let sprites = [
@@ -45,6 +45,7 @@ pub fn single(from: &Mob, to: &mut Mob, params: Params) {
}
to.extra.off_hand = from.extra.off_hand.clone();
to.extra.head = from.extra.head.clone();
to.extra.body = from.extra.body.clone();
to.extra.chest = from.extra.chest.clone();
to.extra.legs = from.extra.legs.clone();
to.extra.feet = from.extra.feet.clone();
+62 -13
View File
@@ -227,22 +227,23 @@ pub fn can_be_baby(s: Species) -> bool {
| S::ZombifiedPiglin
)
}
/// Whether the species keeps its body armour in `extra.head`.
pub fn head_is_body(s: Species) -> bool {
/// The species whose body item Horizon draws and handles as armour (and whose
/// sun-protection slot is the body): the equines (a horse's armour, a llama's
/// carpet, a camel's harness) and the wolf, happy ghast, nautilus and sulfur cube.
pub fn body_slot_species(s: Species) -> bool {
species::body_slot(s) || equine::is_equine(s)
}
/// The item a mob has in an `EquipmentSlot` (by name), with its enchantments.
pub fn slot_item(m: &Mob, slot: &str) -> Option<LootStack> {
let body = head_is_body(m.species());
let (item, index) = match slot {
"mainhand" => (m.extra.main_hand.as_deref(), Some(gear::MAIN)),
"offhand" => (m.extra.off_hand.as_deref(), Some(gear::OFF)),
"head" if !body => (m.extra.head.as_deref(), Some(gear::HEAD)),
"head" => (m.extra.head.as_deref(), Some(gear::HEAD)),
"chest" => (m.extra.chest.as_deref(), Some(gear::CHEST)),
"legs" => (m.extra.legs.as_deref(), Some(gear::LEGS)),
"feet" => (m.extra.feet.as_deref(), Some(gear::FEET)),
"body" if body => (m.extra.head.as_deref(), None),
"body" => (m.extra.body.as_deref(), None),
"saddle" => {
let saddle = m.saddle_item()?;
let mut stack = loot_stack(&saddle.item, 1);
@@ -282,12 +283,12 @@ pub fn slot_item(m: &Mob, slot: &str) -> Option<LootStack> {
Some(stack)
}
/// The `finalize` slot of a drop slot's name, for the slots a stack is kept in.
fn slot_index(m: &Mob, slot: &str) -> Option<usize> {
fn slot_index(slot: &str) -> Option<usize> {
use super::finalize as f;
Some(match slot {
"mainhand" => f::MAINHAND,
"offhand" => f::OFFHAND,
"head" if !head_is_body(m.species()) => f::HEAD,
"head" => f::HEAD,
"chest" => f::CHEST,
"legs" => f::LEGS,
"feet" => f::FEET,
@@ -296,7 +297,7 @@ fn slot_index(m: &Mob, slot: &str) -> Option<usize> {
}
/// The whole stack the mob kept in `slot` (by name), when it kept one.
fn stored_slot_stack(m: &Mob, slot: &str) -> Option<crate::items::ItemStack> {
super::finalize::stored_stack(m, slot_index(m, slot)?)
super::finalize::stored_stack(m, slot_index(slot)?)
}
/// The drop of what the mob has in `slot`, given its `stack` after the drop
/// rolls: the whole stack the mob kept (with the rolled damage), else the loot stack.
@@ -311,7 +312,6 @@ fn slot_drop(m: &Mob, slot: &str, stack: LootStack) -> Drop {
}
/// `setItemSlot(slot, EMPTY)`.
pub fn clear_slot(m: &mut Mob, slot: &str) {
let body = head_is_body(m.species());
let index = match slot {
"mainhand" => {
m.extra.set_main_hand(None);
@@ -321,7 +321,7 @@ pub fn clear_slot(m: &mut Mob, slot: &str) {
m.extra.off_hand = None;
gear::OFF
}
"head" if !body => {
"head" => {
m.extra.head = None;
gear::HEAD
}
@@ -337,8 +337,8 @@ pub fn clear_slot(m: &mut Mob, slot: &str) {
m.extra.feet = None;
gear::FEET
}
"body" if body => {
m.extra.head = None;
"body" => {
m.extra.body = None;
m.extra.body_damage = 0;
m.extra.body_extra = None;
return;
@@ -378,7 +378,10 @@ pub fn drop_chance(m: &Mob, slot: &str) -> f32 {
return chances[index];
}
match slot {
"saddle" | "body" => 2.0,
// A saddle or a body-slot species' worn armour (put on by `Equippable`)
// always drops; a body item elsewhere came from a command or a save.
"saddle" => 2.0,
"body" if body_slot_species(m.species()) => 2.0,
"head" if is_raider(m.species()) && m.extra.head.as_deref() == Some(raider::BANNER) => 2.0,
"mainhand" if m.species() == Species::Vex => 0.0,
_ => DEFAULT_DROP_CHANCE,
@@ -732,3 +735,49 @@ mod death_loot_table_tests {
assert!(items(0, false).iter().all(|i| i != "minecraft:diamond"));
}
}
#[cfg(test)]
mod slot_tests {
use super::*;
use crate::mobs::sensor_scan_tests::kind;
use horizon_core::geometry::Vec3;
use std::sync::Arc;
fn mob(id: &str, species: Species) -> Mob {
let mut kind = kind();
let k = Arc::make_mut(&mut kind);
k.id = id.into();
k.species = species;
Mob::new(1, kind, Vec3::ZERO, 0.0)
}
/// A wolf's armor is the body slot, with the durability it lost; a
/// helmet beside it reads from the head slot.
#[test]
fn a_wolfs_slots_read_apart() {
let mut wolf = mob("minecraft:wolf", Species::Wolf);
wolf.extra.body = Some("minecraft:wolf_armor".into());
wolf.extra.body_damage = 9;
wolf.extra.head = Some("minecraft:iron_helmet".into());
let body = slot_item(&wolf, "body").unwrap();
let head = slot_item(&wolf, "head").unwrap();
assert_eq!(
(body.item.as_str(), body.damage),
("minecraft:wolf_armor", 9)
);
assert_eq!(head.item, "minecraft:iron_helmet");
// The armor was put on by `Equippable`, so it always drops.
assert_eq!(drop_chance(&wolf, "body"), 2.0);
}
/// A body item nothing equipped (a `/item replace ... armor.body` on a
/// zombie) drops by the default chance, as vanilla's unequipped slot.
#[test]
fn a_zombies_body_item_drops_by_the_default_chance() {
let mut zombie = mob("minecraft:zombie", Species::Zombie);
zombie.extra.body = Some("minecraft:iron_helmet".into());
assert_eq!(drop_chance(&zombie, "body"), DEFAULT_DROP_CHANCE);
let body = slot_item(&zombie, "body").unwrap();
assert_eq!(body.item, "minecraft:iron_helmet");
assert!(slot_item(&zombie, "head").is_none());
}
}
@@ -79,7 +79,7 @@ fn setup(m: &mut Mob, baby: bool) {
g.add(5, FloatAround { blocks: 16 });
}
fn wearing_harness(m: &Mob) -> bool {
m.extra.head.is_some()
m.extra.body.is_some()
}
/// `getControllingPassenger`: a player on a harnessed adult.
pub fn controlled(m: &Mob) -> bool {
@@ -91,7 +91,7 @@ pub fn harnessed_adult(m: &Mob) -> bool {
}
/// `getHappyGhastRestrictionRadius`.
fn restriction_radius(m: &Mob) -> i32 {
if !m.is_baby() && m.extra.head.is_none() {
if !m.is_baby() && m.extra.body.is_none() {
64
} else {
32
+7 -9
View File
@@ -3,7 +3,7 @@
//! event `entityEventForEquipmentBreak` gives for the slot, and each client's
//! `LivingEntity.handleEntityEvent` breaks the item it still draws in that
//! slot (`getItemBySlot`), with the sound and the crumbs.
use super::{Mob, drops::head_is_body};
use super::Mob;
use crate::items::ItemStack;
use horizon_world::player_inventory::EquipmentSlot;
@@ -44,7 +44,6 @@ impl Mob {
/// `ClientboundSetEquipmentPacket` gave it where they are kept.
pub fn item_by_slot(&self, slot: EquipmentSlot) -> Option<ItemStack> {
let e = &self.extra;
let body = head_is_body(self.species());
let plain = |id: &Option<std::sync::Arc<str>>| id.as_deref().map(|i| ItemStack::new(i, 1));
let worn = |index: usize, id: &Option<std::sync::Arc<str>>| {
e.armor[index]
@@ -55,18 +54,17 @@ impl Mob {
match slot {
EquipmentSlot::MainHand => e.main_hand_stack().cloned().or_else(|| plain(&e.main_hand)),
EquipmentSlot::OffHand => plain(&e.off_hand),
EquipmentSlot::Head if !body => worn(0, &e.head),
EquipmentSlot::Head => worn(0, &e.head),
EquipmentSlot::Chest => worn(1, &e.chest),
EquipmentSlot::Legs => worn(2, &e.legs),
EquipmentSlot::Feet => worn(3, &e.feet),
// The body armour (a wolf's, a horse's, a llama's carpet) is
// kept in `head`, with the durability it has lost.
EquipmentSlot::Body if body => plain(&e.head).map(|s| ItemStack {
// kept in `body`, with the durability it has lost.
EquipmentSlot::Body => plain(&e.body).map(|s| ItemStack {
damage: e.body_damage.max(0) as u32,
..s
}),
EquipmentSlot::Saddle => self.saddle_item(),
EquipmentSlot::Head | EquipmentSlot::Body => None,
}
}
}
@@ -153,12 +151,12 @@ mod tests {
assert_eq!(item(EquipmentSlot::Saddle), None);
}
/// A wolf's armour is its body slot (kept in `head`), and a horse's
/// A wolf's armour is its body slot (kept in `body`), and a horse's
/// saddle its saddle slot.
#[test]
fn body_armour_and_saddles_break_from_their_slots() {
let mut wolf = mob("minecraft:wolf", Species::Wolf);
wolf.extra.head = Some("minecraft:wolf_armor".into());
wolf.extra.body = Some("minecraft:wolf_armor".into());
wolf.extra.body_damage = 63;
let armour = wolf.item_by_slot(EquipmentSlot::Body).unwrap();
assert_eq!((&*armour.item, armour.damage), ("minecraft:wolf_armor", 63));
@@ -178,7 +176,7 @@ mod tests {
fn a_wolfs_armor_throws_scutes_as_it_cracks() {
use crate::mobs::MobEvent;
let mut wolf = mob("minecraft:wolf", Species::Wolf);
wolf.extra.head = Some("minecraft:wolf_armor".into());
wolf.extra.body = Some("minecraft:wolf_armor".into());
// Levels change below 95%, 69% and 32% of the 64 points left, that is
// after 4, 20 and 44 points of wear.
let hurt = |wolf: &mut Mob, damage: f32| {
+1 -1
View File
@@ -280,7 +280,7 @@ pub fn texture(m: &Mob) -> String {
/// The carpet colour on the llama, if it wears one.
pub(super) fn carpet(m: &Mob) -> Option<&str> {
m.extra
.head
.body
.as_deref()
.and_then(|i| i.strip_prefix("minecraft:"))
.and_then(|i| i.strip_suffix("_carpet"))
+1 -1
View File
@@ -559,7 +559,7 @@ pub fn anim(m: &Mob, s: &State, p: &mut Poses, animations: &HashMap<String, Anim
if let Some(a) = animations.get("NautilusAnimation.SWIMMING") {
a.apply_walk(s.pos + s.age / 5.0, s.speed + 0.2, 2.0, 3.0, p);
}
if m.extra.head.is_some() {
if m.extra.body.is_some() {
p.visible("corals", false);
}
}
+4 -4
View File
@@ -637,7 +637,7 @@ pub fn layers(m: &Mob, age: f32) -> Vec<Layer> {
("wolf#main", "entity/wolf/wolf_collar")
};
// The armor, then the cracks of its damage (`Crackiness.WOLF_ARMOR`).
if !m.is_baby() && m.extra.head.as_deref() == Some("minecraft:wolf_armor") {
if !m.is_baby() && m.extra.body.as_deref() == Some("minecraft:wolf_armor") {
let left = (super::WOLF_ARMOR_DURABILITY - m.extra.body_damage) as f32
/ super::WOLF_ARMOR_DURABILITY as f32;
push(
@@ -672,7 +672,7 @@ pub fn layers(m: &Mob, age: f32) -> Vec<Layer> {
S::HappyGhast => {
let harness = m
.extra
.head
.body
.as_deref()
.and_then(super::happy_ghast::harness_texture);
if !m.is_baby()
@@ -748,7 +748,7 @@ pub fn layers(m: &Mob, age: f32) -> Vec<Layer> {
false,
);
}
if let Some(armor) = m.extra.head.as_deref()
if let Some(armor) = m.extra.body.as_deref()
&& let Some(material) = armor
.strip_prefix("minecraft:")
.and_then(|a| a.strip_suffix("_horse_armor"))
@@ -808,7 +808,7 @@ pub fn layers(m: &Mob, age: f32) -> Vec<Layer> {
S::Nautilus | S::ZombieNautilus if !m.is_baby() => {
if let Some(texture) = m
.extra
.head
.body
.as_deref()
.and_then(super::nautilus::armor_texture)
{
@@ -246,7 +246,7 @@ pub fn swallowable(m: &Mob, item: &str) -> bool {
m.kind.tempt.iter().any(|t| t == item)
}
pub fn has_body(m: &Mob) -> bool {
m.extra.head.is_some()
m.extra.body.is_some()
}
#[cfg(test)]
@@ -266,7 +266,7 @@ mod knockback_sync_tests {
let mut kind = (*crate::mobs::sensor_scan_tests::kind()).clone();
kind.species = Species::SulfurCube;
let mut m = Mob::new(1, Arc::new(kind), Vec3::ZERO, 0.0);
m.extra.head = Some("minecraft:tnt".into());
m.extra.body = Some("minecraft:tnt".into());
assert!(!m.needs_sync);
m.damage(5.0, Damage::player([2.0, 0.0, 0.0]));
assert!(m.velocity.x < 0.0, "velocity {:?}", m.velocity);
@@ -274,7 +274,7 @@ mod knockback_sync_tests {
}
}
pub fn archetype(m: &Mob) -> Option<&'static Archetype> {
m.extra.head.as_deref().and_then(archetype_of)
m.extra.body.as_deref().and_then(archetype_of)
}
/// The archetype whose bounciness, friction and air drag attributes a
/// sulfur cube's physics use (no other mob has those modifiers).
@@ -312,7 +312,7 @@ mod burning_arrow_tests {
let mut kind = (*crate::mobs::sensor_scan_tests::kind()).clone();
kind.species = Species::SulfurCube;
let mut m = Mob::new(1, Arc::new(kind), Vec3::ZERO, 0.0);
m.extra.head = head.map(Into::into);
m.extra.body = head.map(Into::into);
m
}
@@ -384,7 +384,7 @@ mod immune_tests {
let mut kind = (*crate::mobs::sensor_scan_tests::kind()).clone();
kind.species = Species::SulfurCube;
let mut m = Mob::new(1, Arc::new(kind), Vec3::ZERO, 0.0);
m.extra.head = head.map(Into::into);
m.extra.body = head.map(Into::into);
m
}
@@ -1098,7 +1098,7 @@ mod tnt_rule_tests {
let mut kind = (*crate::mobs::sensor_scan_tests::kind()).clone();
kind.species = Species::SulfurCube;
let mut m = Mob::new(1, Arc::new(kind), Vec3::ZERO, 0.0);
m.extra.head = Some("minecraft:tnt".into());
m.extra.body = Some("minecraft:tnt".into());
m
}
@@ -1167,7 +1167,7 @@ mod fuse_anim_tests {
HashSet::from(["minecraft:tnt".to_owned()]),
)]));
let mut m = cube();
m.extra.head = Some("minecraft:tnt".into());
m.extra.body = Some("minecraft:tnt".into());
assert!(prime(&mut m, false));
assert!(m.invulnerable_flag);
let (health, fuse_left) = (m.health, fuse(&m));
@@ -1205,7 +1205,7 @@ mod fuse_anim_tests {
assert_eq!(source.kind, DamageKind::Fireworks);
assert!(!source.no_knockback());
let mut m = cube();
m.extra.head = Some("minecraft:tnt".into());
m.extra.body = Some("minecraft:tnt".into());
let health = m.health;
m.damage(7.0, source);
assert!((15..45).contains(&fuse(&m)), "fuse {}", fuse(&m));