Skip to main content

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}