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 steel_macros::item_behavior;
13use steel_registry::blocks::BlockRef;
14use steel_registry::blocks::block_state_ext::BlockStateExt;
15use steel_registry::blocks::properties::Direction;
16use steel_registry::{REGISTRY, vanilla_game_events};
17use steel_utils::types::UpdateFlags;
18
19use crate::behavior::context::{BlockPlaceContext, InteractionResult, UseOnContext};
20use crate::behavior::{BLOCK_BEHAVIORS, ItemBehavior};
21use crate::entity::Entity;
22use crate::world::game_event::GameEventContext;
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
51impl StandingAndWallBlockItem {
52    /// Creates a new standing and wall block item behavior.
53    ///
54    /// # Arguments
55    /// * `standing_block` - Block placed when looking toward `attachment_direction`
56    /// * `wall_block` - Block placed when looking away from `attachment_direction`
57    /// * `attachment_direction` - Direction that triggers standing block (e.g., `Down` for torches)
58    #[must_use]
59    pub const fn new(
60        standing_block: BlockRef,
61        wall_block: BlockRef,
62        attachment_direction: Direction,
63    ) -> Self {
64        Self {
65            standing_block,
66            wall_block,
67            attachment_direction,
68        }
69    }
70
71    /// Determines which block variant to use based on placement context.
72    ///
73    /// Vanilla caches the wall state once, then iterates through directions to decide
74    /// between standing and wall variants. This matches `StandingAndWallBlockItem.getPlacementState`.
75    #[must_use]
76    pub fn get_placement_state(
77        &self,
78        place_context: &BlockPlaceContext<'_>,
79    ) -> Option<steel_utils::BlockStateId> {
80        let block_behaviors = &*BLOCK_BEHAVIORS;
81
82        // Cache wall state once (vanilla does this before the loop)
83        let wall_state = block_behaviors
84            .get_behavior(self.wall_block)
85            .get_state_for_placement(place_context);
86
87        let directions = place_context.get_nearest_looking_directions();
88        let skip_direction = self.attachment_direction.opposite();
89
90        for direction in directions {
91            // Skip the opposite of attachment direction
92            // (e.g., for torches with attachment_direction=Down, skip Up)
93            if direction == skip_direction {
94                continue;
95            }
96
97            // Choose state based on direction
98            let possible_state = if direction == self.attachment_direction {
99                // Try standing block
100                block_behaviors
101                    .get_behavior(self.standing_block)
102                    .get_state_for_placement(place_context)
103            } else {
104                // Use cached wall state
105                wall_state
106            };
107
108            let Some(state) = possible_state else {
109                continue;
110            };
111
112            // Vanilla's canPlace checks canSurvive (already done in get_state_for_placement)
113            // Then checks isUnobstructed
114            let collision_shape = state.get_collision_shape_at(place_context.place_pos());
115            if place_context
116                .world
117                .is_unobstructed(collision_shape, place_context.place_pos())
118            {
119                return Some(state);
120            }
121        }
122
123        None
124    }
125
126    /// Gets the block reference for a placed state (for sound lookup).
127    #[must_use]
128    pub fn get_block_for_state(&self, state: steel_utils::BlockStateId) -> BlockRef {
129        // Determine which block was placed by checking the state
130        if REGISTRY
131            .blocks
132            .by_state_id(state)
133            .is_some_and(|b| b.key == self.standing_block.key)
134        {
135            self.standing_block
136        } else {
137            self.wall_block
138        }
139    }
140}
141
142impl ItemBehavior for StandingAndWallBlockItem {
143    fn use_on(&self, context: &mut UseOnContext) -> InteractionResult {
144        let mut place_context = context.build_place_context();
145        if !place_context.can_place() {
146            return InteractionResult::Fail;
147        }
148        let place_pos = place_context.place_pos();
149
150        let Some(new_state) = self.get_placement_state(&place_context) else {
151            return InteractionResult::Fail;
152        };
153
154        if !context
155            .world
156            .set_block(place_pos, new_state, UpdateFlags::UPDATE_ALL_IMMEDIATE)
157        {
158            return InteractionResult::Fail;
159        }
160        let placed_state = context.world.get_block_state(place_pos);
161
162        let block = self.get_block_for_state(new_state);
163        let sound_type = &block.config.sound_type;
164        context.world.play_block_sound(
165            sound_type.place_sound,
166            place_pos,
167            sound_type.volume,
168            sound_type.pitch,
169            Some(context.player.id()),
170        );
171        context.world.game_event(
172            &vanilla_game_events::BLOCK_PLACE,
173            place_pos,
174            &GameEventContext::new(Some(context.player), Some(placed_state)),
175        );
176
177        place_context.with_item_mut(|item| item.shrink(1));
178
179        InteractionResult::Success
180    }
181}