mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
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:
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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user