Draw the parrots on the player's shoulders

A parrot that lands on its owner is kept in ShoulderEntityLeft/Right, but
nothing drew it, so it seemed to vanish. Port ParrotOnShoulderLayer and
AbstractClientPlayer.getParrotVariantOnShoulder: the parrot model in
ParrotModel.Pose.ON_SHOULDER with the player's walk animation, age and
head turn, 0.4 to the side and 1.5 (1.3 crouching) up the player's model,
on the player in third person and in the inventory preview.

Part of #3518.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Rx5M6NS763D2WVnb2HmcTH
This commit is contained in:
Claude committed 2026-10-30 18:26:58 +00:00
1 parent a3726f4bf8
commit bec89a8927
2 files changed
+135

No files matched your search

@@ -616,6 +616,7 @@ impl Game<'_> {
}
self.player_elytra_geometry(m, chest, camera, alpha as f32, packed, out);
self.spin_attack_geometry(m, camera, alpha as f32, packed, out);
self.shoulder_parrots_geometry(m, camera, alpha as f32, packed, out);
self.held_item_geometry(m, camera, alpha, packed, true, out);
// CustomHeadLayer: a pumpkin, skull or other head item.
self.head_item_geometry(m, camera, alpha, packed, true, out);
@@ -706,6 +707,7 @@ impl Game<'_> {
self.player_cape_geometry(&m, chest, camera, 1.0, 240 | 240 << 8, &mut verts);
}
self.player_elytra_geometry(&m, chest, camera, 1.0, 240 | 240 << 8, &mut verts);
self.shoulder_parrots_geometry(&m, camera, 1.0, 240 | 240 << 8, &mut verts);
self.held_item_geometry(&m, camera, 1.0, 240 | 240 << 8, false, &mut verts);
self.head_item_geometry(&m, camera, 1.0, 240 | 240 << 8, false, &mut verts);
let height = if m.extra.crouching { 1.5 } else { 1.8 };
@@ -888,6 +890,33 @@ impl Game<'_> {
out,
);
}
/// `ParrotOnShoulderLayer`: the parrots the player keeps in its
/// `ShoulderEntityLeft` and `ShoulderEntityRight` (game/shoulders.rs).
pub(super) fn shoulder_parrots_geometry(
&self,
m: &Mob,
camera: glam::DVec3,
partial: f32,
light: u32,
out: &mut Vec<horizon_client::mesher::Vertex>,
) {
let Some(extra) = self.level.player_extra.as_ref() else {
return;
};
let Some(parrot) = self.res.mob_types().get("minecraft:parrot") else {
return;
};
let atlas = &self.gui.atlas;
let lookup = |name: &str| atlas.sprites.get(name).map(|r| [r.u0, r.v0, r.u1, r.v1]);
let base = horizon_client::mobs::render::base_transform(m, partial, camera);
for (key, left) in [("ShoulderEntityLeft", true), ("ShoulderEntityRight", false)] {
if let Some(variant) = shoulder_parrot_variant(extra, key) {
horizon_client::mobs::render::shoulder_parrot(
m, parrot, variant, left, partial, base, &lookup, light, out,
);
}
}
}
/// `SpinAttackEffectLayer`: Riptide's two boxes turning around a spinning
/// player (`SpinAttackEffectModel.setupAnim`).
pub(super) fn spin_attack_geometry(
@@ -931,6 +960,22 @@ impl Game<'_> {
}
}
/// `AbstractClientPlayer.getParrotVariantOnShoulder`: the variant of the
/// parrot kept in the `key` shoulder slot, when one is (a parrot's saved
/// compound, whose `Variant` defaults to 0).
fn shoulder_parrot_variant(extra: &horizon_world::nbt::Tag, key: &str) -> Option<i32> {
use horizon_world::nbt::Tag;
let parrot = extra.get(key)?;
match parrot.get("id") {
Some(Tag::String(id)) if id.to_string_lossy() == "minecraft:parrot" => {}
_ => return None,
}
Some(match parrot.get("Variant") {
Some(Tag::Int(v)) => *v,
_ => 0,
})
}
fn player_preview_quads(
mut verts: Vec<horizon_client::mesher::Vertex>,
gui: glam::Mat4,
@@ -1033,6 +1078,39 @@ fn player_walk_distance(from: Vec3, to: Vec3) -> f32 {
mod preview_tests {
use super::*;
#[test]
fn shoulder_slots_hold_parrots_by_variant() {
use horizon_world::nbt::Tag;
let parrot = |variant: Option<i32>, id: &str| {
let mut entries = vec![("id", Tag::String(id.into()))];
if let Some(v) = variant {
entries.push(("Variant", Tag::Int(v)));
}
Tag::compound(entries)
};
let extra = Tag::compound([
("ShoulderEntityLeft", parrot(Some(3), "minecraft:parrot")),
("ShoulderEntityRight", parrot(None, "minecraft:parrot")),
]);
assert_eq!(
shoulder_parrot_variant(&extra, "ShoulderEntityLeft"),
Some(3)
);
// A parrot saved without a variant is the first one.
assert_eq!(
shoulder_parrot_variant(&extra, "ShoulderEntityRight"),
Some(0)
);
let empty = Tag::compound([
(
"ShoulderEntityLeft",
Tag::compound(Vec::<(&str, Tag)>::new()),
),
("ShoulderEntityRight", parrot(Some(1), "minecraft:cat")),
]);
assert_eq!(shoulder_parrot_variant(&empty, "ShoulderEntityLeft"), None);
assert_eq!(shoulder_parrot_variant(&empty, "ShoulderEntityRight"), None);
}
#[test]
fn the_player_model_rises_with_the_camera_at_levitation_255() {
// LivingEntity.travel at Levitation 255: 0.05 * 255 blocks a tick.
let from = Vec3::new(0.5, 80.0, 0.5);
+57
View File
@@ -1328,6 +1328,54 @@ pub fn cape(
// RenderTypes.entitySolid with OverlayTexture.NO_OVERLAY.
draw(model, base, &poses, uv, 0xFFFF_FFFF, light, 0, out);
}
/// `ParrotOnShoulderLayer.submitOnShoulder`: a parrot of `variant` on the
/// avatar's left or right shoulder, in `ParrotModel.Pose.ON_SHOULDER` with
/// the avatar's walk animation, age and head turn. `base` is the avatar's
/// root transform; `parrot` the parrot's entity type (its model).
#[allow(clippy::too_many_arguments)]
pub fn shoulder_parrot(
avatar: &Mob,
parrot: &super::MobType,
variant: i32,
left: bool,
alpha: f32,
base: glam::Mat4,
atlas: Atlas,
light: u32,
out: &mut Vec<Vertex>,
) {
let Some(uv) = atlas(&shoulder_parrot_texture(variant)) else {
return;
};
let model = parrot.layer(&parrot.model_key()).unwrap_or(&parrot.model);
let s = anim::State {
baby: false,
..anim::state(avatar, alpha)
};
let mut poses = Poses::new(model);
// ParrotRenderState.flapAngle stays 0.
super::body_anim::parrot(
&mut poses,
&s,
super::body_anim::ParrotPose::OnShoulder,
0.0,
);
let y = if avatar.extra.crouching { -1.3 } else { -1.5 };
let x = if left { 0.4 } else { -0.4 };
let base = base * glam::Mat4::from_translation(glam::Vec3::new(x, y, 0.0));
// ParrotModel's cutout render type with OverlayTexture.NO_OVERLAY.
draw(model, base, &poses, uv, 0xFFFF_FFFF, light, 0, out);
}
/// `ParrotRenderer.getVariantTexture` for `Parrot.Variant.byId` (an unknown
/// id is the first variant).
fn shoulder_parrot_texture(variant: i32) -> String {
const NAMES: [&str; 5] = ["red_blue", "blue", "green", "yellow_blue", "grey"];
let name = usize::try_from(variant)
.ok()
.and_then(|i| NAMES.get(i))
.unwrap_or(&NAMES[0]);
format!("entity/parrot/parrot_{name}")
}
/// `render_with` without the body, for an avatar the viewer can't see:
/// `LivingEntityRenderer.submit` gets no render type for an invisible body
/// (`isBodyVisible`, `isInvisibleTo`), but its layers still draw
@@ -1831,6 +1879,15 @@ pub fn held_item_transform(
mod tests {
use super::*;
#[test]
fn shoulder_parrot_textures_follow_the_variant() {
assert_eq!(shoulder_parrot_texture(0), "entity/parrot/parrot_red_blue");
assert_eq!(shoulder_parrot_texture(4), "entity/parrot/parrot_grey");
// Parrot.Variant.byId: out of range is the first variant.
assert_eq!(shoulder_parrot_texture(5), "entity/parrot/parrot_red_blue");
assert_eq!(shoulder_parrot_texture(-1), "entity/parrot/parrot_red_blue");
}
/// VillagerProfessionLayer's textures (type, or baby for a baby;
/// profession; level) of every villager and zombie villager are in the
/// prepared resources: a missing one draws nothing.