mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Show the armour stand preview in the smithing table
SmithingScreen.updateArmorStandPreview puts the result on a stand without its base plate and with its arms (armour in its slot, anything else in the off hand), drawn by extractEntityInInventory beside the slots. The world's armour stand drawing is split out per stand so the screen can reuse it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Rx5M6NS763D2WVnb2HmcTH
This commit is contained in:
3 files changed
+221
-133
No files matched your search
@@ -715,12 +715,6 @@ impl Game<'_> {
|
||||
alpha: f64,
|
||||
out: &mut Vec<Vertex>,
|
||||
) {
|
||||
let assets = &self.armor_stand_assets;
|
||||
let sprites = &self.gui.atlas.sprites;
|
||||
let uv_of = |name: &str| sprites.get(name).map(|r| [r.u0, r.v0, r.u1, r.v1]);
|
||||
let Some(wood) = uv_of("entity/armorstand/armorstand") else {
|
||||
return;
|
||||
};
|
||||
for a in &self.armor_stands {
|
||||
let entity_start = out.len();
|
||||
// ArmorStand.shouldRenderAtSqrDistance: 256 times the box's mean
|
||||
@@ -753,9 +747,6 @@ impl Game<'_> {
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let Some(model) = (if a.small { &assets.small } else { &assets.main }) else {
|
||||
continue;
|
||||
};
|
||||
let p = glam::DVec3::new(a.position.x, a.position.y, a.position.z) - camera;
|
||||
// setupRotations: 180 - body yaw, plus the wobble for 5 ticks after a hit.
|
||||
let wiggle = (self.world.game_time - a.last_hit) as f32 + alpha as f32;
|
||||
@@ -768,143 +759,224 @@ impl Game<'_> {
|
||||
* glam::Mat4::from_rotation_y((180.0 - a.yaw + wobble).to_radians())
|
||||
* glam::Mat4::from_scale(glam::Vec3::new(-1.0, -1.0, 1.0))
|
||||
* glam::Mat4::from_translation(glam::Vec3::new(0.0, -1.501, 0.0));
|
||||
let mut poses = Poses::new(model);
|
||||
// ArmorStandModel.setupAnim: the stand's pose and part visibility.
|
||||
apply_pose(&mut poses, &a.pose);
|
||||
// Item layers pose from the hidden arms too (`visible` only skips drawing).
|
||||
let posed = poses.clone();
|
||||
let mut hide = |name: &str| {
|
||||
if poses.has(name) {
|
||||
let pose = poses.get(name);
|
||||
pose[6] = 0.0;
|
||||
pose[7] = 0.0;
|
||||
pose[8] = 0.0;
|
||||
}
|
||||
};
|
||||
if !a.show_arms {
|
||||
hide("left_arm");
|
||||
hide("right_arm");
|
||||
}
|
||||
if a.no_base_plate {
|
||||
hide("base_plate");
|
||||
}
|
||||
let light = self.world.light(
|
||||
a.position.x.floor() as i32,
|
||||
(a.position.y + 0.5).floor() as i32,
|
||||
a.position.z.floor() as i32,
|
||||
);
|
||||
let packed = (u32::from(light >> 4) * 16) | ((u32::from(light & 15) * 16) << 8);
|
||||
// LivingEntityRenderer.submit: an invisible stand has no body render type, but its
|
||||
// layers (held items, custom head, armour) are submitted all the same.
|
||||
if !a.invisible {
|
||||
self.armor_stand_vertices(a, base, packed, out);
|
||||
self.cull_entity_vertices(out, entity_start);
|
||||
}
|
||||
}
|
||||
/// `ArmorStandRenderer.submit` for stand `a` under `base` (its
|
||||
/// `setupRotations`): the body, the hand and head items, the armour and
|
||||
/// the wings, at packed light `packed`.
|
||||
fn armor_stand_vertices(
|
||||
&self,
|
||||
a: &ArmorStand,
|
||||
base: glam::Mat4,
|
||||
packed: u32,
|
||||
out: &mut Vec<Vertex>,
|
||||
) {
|
||||
let assets = &self.armor_stand_assets;
|
||||
let sprites = &self.gui.atlas.sprites;
|
||||
let uv_of = |name: &str| sprites.get(name).map(|r| [r.u0, r.v0, r.u1, r.v1]);
|
||||
let Some(wood) = uv_of("entity/armorstand/armorstand") else {
|
||||
return;
|
||||
};
|
||||
let Some(model) = (if a.small { &assets.small } else { &assets.main }) else {
|
||||
return;
|
||||
};
|
||||
let mut poses = Poses::new(model);
|
||||
// ArmorStandModel.setupAnim: the stand's pose and part visibility.
|
||||
apply_pose(&mut poses, &a.pose);
|
||||
// Item layers pose from the hidden arms too (`visible` only skips drawing).
|
||||
let posed = poses.clone();
|
||||
let mut hide = |name: &str| {
|
||||
if poses.has(name) {
|
||||
let pose = poses.get(name);
|
||||
pose[6] = 0.0;
|
||||
pose[7] = 0.0;
|
||||
pose[8] = 0.0;
|
||||
}
|
||||
};
|
||||
if !a.show_arms {
|
||||
hide("left_arm");
|
||||
hide("right_arm");
|
||||
}
|
||||
if a.no_base_plate {
|
||||
hide("base_plate");
|
||||
}
|
||||
// LivingEntityRenderer.submit: an invisible stand has no body render type, but its
|
||||
// layers (held items, custom head, armour) are submitted all the same.
|
||||
if !a.invisible {
|
||||
horizon_client::mobs::render::draw(
|
||||
model,
|
||||
base,
|
||||
&poses,
|
||||
wood,
|
||||
0xFFFF_FFFF,
|
||||
packed,
|
||||
0,
|
||||
out,
|
||||
);
|
||||
}
|
||||
self.armor_stand_items(a, model, base, &posed, packed, out);
|
||||
// HumanoidArmorLayer: head, chest, legs, feet.
|
||||
for (slot, layer) in [
|
||||
(5usize, "helmet"),
|
||||
(4, "chestplate"),
|
||||
(3, "leggings"),
|
||||
(2, "boots"),
|
||||
] {
|
||||
let Some(stack) = &a.equipment[slot] else {
|
||||
continue;
|
||||
};
|
||||
let Some(asset) = self
|
||||
.res
|
||||
.items()
|
||||
.get(&stack.item)
|
||||
.and_then(|i| i.equip_asset.clone())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let kind = if layer == "leggings" {
|
||||
"humanoid_leggings"
|
||||
} else {
|
||||
"humanoid"
|
||||
};
|
||||
let Some(layers) = assets.equipment.get(&asset).and_then(|k| k.get(kind)) else {
|
||||
continue;
|
||||
};
|
||||
let Some(part) = assets.layers.get(&(layer, a.small)) else {
|
||||
continue;
|
||||
};
|
||||
// ArmorStandRenderer's HumanoidArmorLayer: the armour glint
|
||||
// when `stack.hasFoil()`, as on a living entity.
|
||||
let item_glint = self
|
||||
.res
|
||||
.items()
|
||||
.enchantments
|
||||
.props
|
||||
.get(&stack.item)
|
||||
.is_some_and(|p| p.glint);
|
||||
let overlay = if stack.has_foil(item_glint) {
|
||||
horizon_client::mobs::render::ARMOR_GLINT
|
||||
} else {
|
||||
0
|
||||
};
|
||||
// ArmorStandArmorModel.setupAnim: the same pose as the stand.
|
||||
let mut layer_poses = Poses::new(part);
|
||||
apply_pose(&mut layer_poses, &a.pose);
|
||||
// EquipmentLayerRenderer: every layer of the asset, the leather overlay
|
||||
// too; the shader glint goes on the first layer only (renderShaderGlint).
|
||||
for (i, (texture, undyed)) in layers.iter().enumerate() {
|
||||
let Some(uv) = uv_of(texture) else {
|
||||
continue;
|
||||
};
|
||||
let color = layer_color(*undyed, Some(stack));
|
||||
horizon_client::mobs::render::draw(
|
||||
model,
|
||||
part,
|
||||
base,
|
||||
&poses,
|
||||
wood,
|
||||
0xFFFF_FFFF,
|
||||
&layer_poses,
|
||||
uv,
|
||||
color,
|
||||
packed,
|
||||
0,
|
||||
if i == 0 { overlay } else { 0 },
|
||||
out,
|
||||
);
|
||||
}
|
||||
self.armor_stand_items(a, model, base, &posed, packed, out);
|
||||
// HumanoidArmorLayer: head, chest, legs, feet.
|
||||
for (slot, layer) in [
|
||||
(5usize, "helmet"),
|
||||
(4, "chestplate"),
|
||||
(3, "leggings"),
|
||||
(2, "boots"),
|
||||
] {
|
||||
let Some(stack) = &a.equipment[slot] else {
|
||||
continue;
|
||||
};
|
||||
let Some(asset) = self
|
||||
.res
|
||||
.items()
|
||||
.get(&stack.item)
|
||||
.and_then(|i| i.equip_asset.clone())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let kind = if layer == "leggings" {
|
||||
"humanoid_leggings"
|
||||
} else {
|
||||
"humanoid"
|
||||
};
|
||||
let Some(layers) = assets.equipment.get(&asset).and_then(|k| k.get(kind)) else {
|
||||
continue;
|
||||
};
|
||||
let Some(part) = assets.layers.get(&(layer, a.small)) else {
|
||||
continue;
|
||||
};
|
||||
// ArmorStandRenderer's HumanoidArmorLayer: the armour glint
|
||||
// when `stack.hasFoil()`, as on a living entity.
|
||||
let item_glint = self
|
||||
.res
|
||||
.items()
|
||||
.enchantments
|
||||
.props
|
||||
.get(&stack.item)
|
||||
.is_some_and(|p| p.glint);
|
||||
let overlay = if stack.has_foil(item_glint) {
|
||||
horizon_client::mobs::render::ARMOR_GLINT
|
||||
} else {
|
||||
0
|
||||
};
|
||||
// ArmorStandArmorModel.setupAnim: the same pose as the stand.
|
||||
let mut layer_poses = Poses::new(part);
|
||||
apply_pose(&mut layer_poses, &a.pose);
|
||||
// EquipmentLayerRenderer: every layer of the asset, the leather overlay
|
||||
// too; the shader glint goes on the first layer only (renderShaderGlint).
|
||||
for (i, (texture, undyed)) in layers.iter().enumerate() {
|
||||
let Some(uv) = uv_of(texture) else {
|
||||
continue;
|
||||
};
|
||||
let color = layer_color(*undyed, Some(stack));
|
||||
horizon_client::mobs::render::draw(
|
||||
part,
|
||||
base,
|
||||
&layer_poses,
|
||||
uv,
|
||||
color,
|
||||
packed,
|
||||
if i == 0 { overlay } else { 0 },
|
||||
out,
|
||||
);
|
||||
}
|
||||
// EquipmentLayerRenderer: the trim layer over the armour.
|
||||
if let Some(trim) = assets
|
||||
.trim_texture(&asset, kind, stack)
|
||||
.and_then(|t| uv_of(&t.sprite()))
|
||||
{
|
||||
horizon_client::mobs::render::draw(
|
||||
part,
|
||||
base,
|
||||
&layer_poses,
|
||||
trim,
|
||||
0xFFFF_FFFF,
|
||||
packed,
|
||||
overlay,
|
||||
out,
|
||||
);
|
||||
}
|
||||
// EquipmentLayerRenderer: the trim layer over the armour.
|
||||
if let Some(trim) = assets
|
||||
.trim_texture(&asset, kind, stack)
|
||||
.and_then(|t| uv_of(&t.sprite()))
|
||||
{
|
||||
horizon_client::mobs::render::draw(
|
||||
part,
|
||||
base,
|
||||
&layer_poses,
|
||||
trim,
|
||||
0xFFFF_FFFF,
|
||||
packed,
|
||||
overlay,
|
||||
out,
|
||||
);
|
||||
}
|
||||
// WingsLayer: ArmorStandRenderer adds it after the armour layers. A stand
|
||||
// never crouches or glides, so its wings rest folded.
|
||||
self.wings_geometry(
|
||||
a.equipment[4].as_ref().map(|s| &*s.item),
|
||||
None,
|
||||
a.small,
|
||||
base,
|
||||
horizon_client::mobs::ElytraAnimation::RESTING,
|
||||
false,
|
||||
packed,
|
||||
self.wings_overlay(a.equipment[4].as_ref()),
|
||||
out,
|
||||
);
|
||||
self.cull_entity_vertices(out, entity_start);
|
||||
}
|
||||
// WingsLayer: ArmorStandRenderer adds it after the armour layers. A stand
|
||||
// never crouches or glides, so its wings rest folded.
|
||||
self.wings_geometry(
|
||||
a.equipment[4].as_ref().map(|s| &*s.item),
|
||||
None,
|
||||
a.small,
|
||||
base,
|
||||
horizon_client::mobs::ElytraAnimation::RESTING,
|
||||
false,
|
||||
packed,
|
||||
self.wings_overlay(a.equipment[4].as_ref()),
|
||||
out,
|
||||
);
|
||||
}
|
||||
/// `SmithingScreen`'s armour stand: `updateArmorStandPreview` puts the
|
||||
/// result on a stand without its base plate and with its arms, turned to
|
||||
/// 210 degrees, and `extractEntityInInventory` draws it at scale 25 in
|
||||
/// the box (x0, y0) to (x1, y1), moved up 1 and tilted by `ARMOR_STAND_ANGLE`.
|
||||
pub(super) fn draw_smithing_armor_stand(
|
||||
&self,
|
||||
batch: &mut horizon_client::hud::HudBatch,
|
||||
result: Option<&ItemStack>,
|
||||
[x0, y0, x1, y1]: [f32; 4],
|
||||
) {
|
||||
let mut stand = ArmorStand::new(0, Vec3::ZERO, 210.0);
|
||||
stand.no_base_plate = true;
|
||||
stand.show_arms = true;
|
||||
if let Some(stack) = result {
|
||||
let equip = self
|
||||
.res
|
||||
.items()
|
||||
.get(&stack.item)
|
||||
.and_then(|i| i.equip_slot.clone());
|
||||
if let Some(slot) = preview_slot(equip.as_deref()) {
|
||||
stand.equipment[slot] = Some(stack.clone());
|
||||
}
|
||||
}
|
||||
// LivingEntityRenderer: setupRotations (180 - body yaw), the flip and
|
||||
// the 1.501 drop, at full light.
|
||||
let base = glam::Mat4::from_rotation_y((180.0 - stand.yaw).to_radians())
|
||||
* glam::Mat4::from_scale(glam::Vec3::new(-1.0, -1.0, 1.0))
|
||||
* glam::Mat4::from_translation(glam::Vec3::new(0.0, -1.501, 0.0));
|
||||
let mut verts = Vec::new();
|
||||
self.armor_stand_vertices(&stand, base, 240 | 240 << 8, &mut verts);
|
||||
// ARMOR_STAND_ANGLE: rotationXYZ(0.43633232, 0, PI).
|
||||
let gui =
|
||||
glam::Mat4::from_translation(glam::Vec3::new((x0 + x1) / 2.0, (y0 + y1) / 2.0, 0.0))
|
||||
* glam::Mat4::from_scale(glam::Vec3::new(25.0, 25.0, -25.0))
|
||||
* glam::Mat4::from_translation(glam::Vec3::new(0.0, 1.0, 0.0))
|
||||
* glam::Mat4::from_rotation_x(0.436_332_32)
|
||||
* glam::Mat4::from_rotation_z(std::f32::consts::PI);
|
||||
let quads =
|
||||
super::perspective::player_preview_quads(verts, gui, None, &self.gui.atlas.sprites);
|
||||
let start = batch.vertices.len();
|
||||
for (_, p, uv, color) in quads {
|
||||
batch.quad_3d(p, uv, color);
|
||||
}
|
||||
batch.depth_since(start, [x0, y0, x1, y1]);
|
||||
}
|
||||
}
|
||||
/// The stand's slot (`ArmorStand::SLOTS` order) for an item whose
|
||||
/// `equippable` slot is `equip`: its own, or the off hand when it has none
|
||||
/// (`SmithingScreen.updateArmorStandPreview`). A slot a stand has no place
|
||||
/// for (a horse's body, a saddle) shows nothing.
|
||||
fn preview_slot(equip: Option<&str>) -> Option<usize> {
|
||||
match equip {
|
||||
None | Some("offhand") => Some(1),
|
||||
Some("mainhand") => Some(0),
|
||||
Some("feet") => Some(2),
|
||||
Some("legs") => Some(3),
|
||||
Some("chest") => Some(4),
|
||||
Some("head") => Some(5),
|
||||
Some(_) => None,
|
||||
}
|
||||
}
|
||||
/// The `Pose` rotations on the parts of an armor stand model or its armor
|
||||
@@ -1183,6 +1255,17 @@ mod burn_drop_tests {
|
||||
#[cfg(test)]
|
||||
mod layer_color_tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn the_smithing_preview_wears_armour_and_holds_the_rest_in_its_off_hand() {
|
||||
assert_eq!(preview_slot(Some("head")), Some(5));
|
||||
assert_eq!(preview_slot(Some("chest")), Some(4));
|
||||
assert_eq!(preview_slot(Some("legs")), Some(3));
|
||||
assert_eq!(preview_slot(Some("feet")), Some(2));
|
||||
// A netherite sword or tool has no equippable slot.
|
||||
assert_eq!(preview_slot(None), Some(1));
|
||||
assert_eq!(preview_slot(Some("body")), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dyed_stack_overrides_the_undyed_leather_colour() {
|
||||
let mut dyed = ItemStack::new("minecraft:leather_helmet", 1);
|
||||
|
||||
@@ -931,7 +931,7 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn player_preview_quads(
|
||||
pub(super) fn player_preview_quads(
|
||||
mut verts: Vec<horizon_client::mesher::Vertex>,
|
||||
gui: glam::Mat4,
|
||||
fire: Option<(f32, f32)>,
|
||||
|
||||
@@ -1744,6 +1744,11 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
Kind::Smithing => {
|
||||
self.draw_smithing_armor_stand(
|
||||
batch,
|
||||
s.result.as_ref(),
|
||||
[ox + 121.0, oy + 20.0, ox + 161.0, oy + 80.0],
|
||||
);
|
||||
// `CyclingSlotBackground.extractRenderState`: an empty slot shows the
|
||||
// icons of what the template in the table takes.
|
||||
for (i, cycling) in s.cycling.iter().enumerate() {
|
||||
|
||||
Reference in new issue
Block a user