steel_core/behavior/items/standing_and_wall_block_item.rs
1//! Standing and wall block item behavior implementation.
2//!
3//! This handles items like torches that place different block variants
4//! depending on whether they're placed on top of a block (standing) or
5//! on the side of a block (wall).
6//!
7//! **Vanilla differences:** None - this matches vanilla's `StandingAndWallBlockItem` exactly.
8//! The placement logic iterates through `getNearestLookingDirections()` and tries each
9//! direction (skipping the opposite of `attachmentDirection`), using the standing block
10//! when direction matches `attachmentDirection` and wall block otherwise.
11
12use crate::behavior::context::{BlockPlaceContext, InteractionResult, UseOnContext};
13use crate::behavior::items::block_item::SurvivalCheck;
14use crate::behavior::{BLOCK_BEHAVIORS, BlockItem, ItemBehavior};
15use crate::world::World;
16use std::sync::Arc;
17use steel_macros::item_behavior;
18use steel_registry::REGISTRY;
19use steel_registry::blocks::BlockRef;
20use steel_registry::blocks::block_state_ext::BlockStateExt;
21use steel_registry::blocks::properties::Direction;
22use steel_utils::{BlockPos, BlockStateId};
23
24/// Behavior for items that place either a standing or wall variant of a block.
25///
26/// Used for torches (`torch/wall_torch`), soul torches, copper torches, etc.
27/// When placed looking down (toward `attachmentDirection`), places the standing variant.
28/// When placed looking horizontally or up, places the wall variant.
29///
30/// The `attachmentDirection` is typically `Direction::Down` for torches, meaning:
31/// - Looking down → place standing torch on top of block below
32/// - Looking horizontally → place wall torch on side of block
33#[item_behavior]
34pub struct StandingAndWallBlockItem {
35 /// The block to place when looking toward `attachmentDirection` (e.g., `torch`).
36 #[json_arg(vanilla_blocks, json = "block")]
37 pub standing_block: BlockRef,
38 /// The block to place otherwise (e.g., `wall_torch`).
39 #[json_arg(vanilla_blocks, json = "wall_block")]
40 pub wall_block: BlockRef,
41 /// The direction that triggers the standing block placement.
42 /// For torches this is `Direction::Down` - when looking down, place standing torch.
43 #[json_arg(
44 r#enum = "Direction",
45 module = "steel_registry::blocks::properties",
46 json = "attachment_direction"
47 )]
48 pub attachment_direction: Direction,
49
50 /// The block that this item normally places. This is usually the standing block.
51 pub base: BlockItem,
52}
53
54impl StandingAndWallBlockItem {
55 /// Creates a new standing and wall block item behavior.
56 ///
57 /// # Arguments
58 /// * `standing_block` - Block placed when looking toward `attachment_direction`
59 /// * `wall_block` - Block placed when looking away from `attachment_direction`
60 /// * `attachment_direction` - Direction that triggers standing block (e.g., `Down` for torches)
61 #[must_use]
62 pub const fn new(
63 standing_block: BlockRef,
64 wall_block: BlockRef,
65 attachment_direction: Direction,
66 ) -> Self {
67 Self {
68 standing_block,
69 wall_block,
70 attachment_direction,
71 base: BlockItem::new(standing_block),
72 }
73 }
74
75 /// Determines which block variant to use based on placement context.
76 ///
77 /// Vanilla caches the wall state once, then iterates through directions to decide
78 /// between standing and wall variants. This matches `StandingAndWallBlockItem.getPlacementState`.
79 #[must_use]
80 pub fn get_placement_state(
81 &self,
82 place_context: &BlockPlaceContext<'_>,
83 can_place: impl Fn(&Arc<World>, BlockStateId, BlockPos) -> bool,
84 ) -> Option<BlockStateId> {
85 let block_behaviors = &*BLOCK_BEHAVIORS;
86
87 // Cache wall state once (vanilla does this before the loop)
88 let wall_state = block_behaviors
89 .get_behavior(self.wall_block)
90 .get_state_for_placement(place_context);
91
92 let directions = place_context.get_nearest_looking_directions();
93 let skip_direction = self.attachment_direction.opposite();
94 let place_pos = place_context.place_pos();
95
96 for direction in directions {
97 // Skip the opposite of attachment direction
98 // (e.g., for torches with attachment_direction=Down, skip Up)
99 if direction == skip_direction {
100 continue;
101 }
102
103 // Choose state based on direction
104 let possible_state = if direction == self.attachment_direction {
105 // Try standing block
106 block_behaviors
107 .get_behavior(self.standing_block)
108 .get_state_for_placement(place_context)
109 } else {
110 // Use cached wall state
111 wall_state
112 };
113
114 let Some(state) = possible_state else {
115 continue;
116 };
117
118 if !can_place(place_context.world, state, place_pos) {
119 continue;
120 }
121
122 // Vanilla's canPlace checks canSurvive (already done in get_state_for_placement)
123 // Then checks isUnobstructed
124 let collision_shape = state.get_collision_shape_at(place_context.place_pos());
125 if place_context
126 .world
127 .is_unobstructed(collision_shape, place_context.place_pos())
128 {
129 return Some(state);
130 }
131 }
132
133 None
134 }
135
136 /// Gets the block reference for a placed state (for sound lookup).
137 #[must_use]
138 pub fn get_block_for_state(&self, state: steel_utils::BlockStateId) -> BlockRef {
139 // Determine which block was placed by checking the state
140 if REGISTRY
141 .blocks
142 .by_state_id(state)
143 .is_some_and(|b| b.key == self.standing_block.key)
144 {
145 self.standing_block
146 } else {
147 self.wall_block
148 }
149 }
150}
151
152impl ItemBehavior for StandingAndWallBlockItem {
153 // Survival check required here because it was a true function and this should be better
154 fn use_on(&self, context: &mut UseOnContext) -> InteractionResult {
155 self.base.place_with_policy(
156 context.build_place_context(),
157 Some,
158 SurvivalCheck::Skipped,
159 BlockItem::place_block,
160 self.standing_block.config.sound_type.place_sound,
161 |context| self.get_placement_state(context, |_, _, _| true),
162 )
163 }
164}