Skip to main content

steel_core/behavior/items/
block_transformer.rs

1use glam::DVec3;
2use steel_protocol::packets::game::SoundSource;
3use steel_registry::block_transformer::{
4    BlockTransformData, DropStrategy, TransformParticle, TransformType,
5};
6use steel_registry::blocks::block_state_ext::BlockStateExt as _;
7use steel_registry::data_components::vanilla_components::{BLOCK_TRANSFORMER, BLOCKS_ATTACKS};
8use steel_registry::item_stack::ItemStack;
9use steel_registry::items::ItemRef;
10use steel_registry::vanilla_block_tags::BlockTag;
11use steel_registry::{REGISTRY, RegistryExt as _, level_events, vanilla_game_events};
12use steel_utils::BlockStateId;
13use steel_utils::random::legacy_random::LegacyRandom;
14use steel_utils::types::{InteractionHand, UpdateFlags};
15
16use crate::behavior::block::drop_from_block_interact_loot_table;
17use crate::behavior::{InteractionResult, UseOnContext};
18use crate::block_state_provider::BlockStateProviderEvaluator;
19use crate::entity::{Entity as _, LivingEntity as _};
20use crate::inventory::equipment::EquipmentSlot;
21use crate::player::Player;
22use crate::world::game_event::GameEventContext;
23
24use super::copper_chest_events::emit_connected_chest_block_change;
25
26pub(crate) fn use_on(context: &mut UseOnContext) -> InteractionResult {
27    let transformer = context.inv.with_item(|item| {
28        item.get(BLOCK_TRANSFORMER)
29            .map(|component| component.block_transformer)
30    });
31
32    let Some(transformer) = transformer else {
33        return InteractionResult::Pass;
34    };
35
36    if player_has_blocking_item_use_intent(context) {
37        return InteractionResult::Pass;
38    }
39
40    let pos = context.hit_result.block_pos;
41    let mut random = LegacyRandom::from_seed(rand::random());
42
43    for transform in &transformer.transforms {
44        if transform
45            .disallowed_faces
46            .contains(&context.hit_result.direction)
47        {
48            continue;
49        }
50
51        let Some(new_state) = BlockStateProviderEvaluator::sample_block_state_provider_optional(
52            context.world.as_ref(),
53            &REGISTRY,
54            &mut random,
55            &transform.block_state_provider,
56            pos,
57        ) else {
58            continue;
59        };
60
61        let new_state = if transform.update_from_neighbors {
62            context.world.update_from_neighbor_shapes(new_state, pos)
63        } else {
64            new_state
65        };
66
67        // TODO: Trigger ITEM_USED_ON_BLOCK advancements here
68        let old_state = context.world.get_block_state(pos);
69        drop_loot(transform, context, old_state);
70        let broken_item = context.inv.with_item(|item| {
71            consume_transform_item(item, transform, context.player.has_infinite_materials())
72        });
73
74        if let Some(item) = broken_item {
75            let slot = match context.hand {
76                InteractionHand::MainHand => EquipmentSlot::MainHand,
77                InteractionHand::OffHand => EquipmentSlot::OffHand,
78            };
79            context.player.on_equipped_item_broken(item, slot);
80        }
81
82        context
83            .world
84            .set_block(pos, new_state, UpdateFlags::UPDATE_ALL_IMMEDIATE);
85
86        let (x, y, z) = pos.get_center();
87        context.world.play_sound_holder_at(
88            &transform.sound,
89            SoundSource::Blocks,
90            DVec3::new(x, y, z),
91            1.0,
92            1.0,
93            Some(context.player.id()),
94        );
95
96        let particle_event = particle_event(transform.particle);
97        if let Some(event) = particle_event {
98            context
99                .world
100                .level_event(event, pos, 0, Some(context.player.id()));
101        }
102
103        let player: &Player = context.player;
104        context.world.game_event(
105            &vanilla_game_events::BLOCK_CHANGE,
106            pos,
107            &GameEventContext::new(Some(player), Some(new_state)),
108        );
109
110        if transform.transform_type == TransformType::CopperChest
111            && old_state.get_block().has_tag(&BlockTag::COPPER_CHESTS)
112        {
113            emit_connected_chest_block_change(
114                context.world,
115                pos,
116                old_state,
117                context.player,
118                particle_event,
119            );
120        }
121
122        return InteractionResult::Success;
123    }
124    InteractionResult::Pass
125}
126
127fn player_has_blocking_item_use_intent(context: &UseOnContext) -> bool {
128    context.hand == InteractionHand::MainHand
129        && !context.player.is_secondary_use_active()
130        && context
131            .player
132            .inventory
133            .lock()
134            .get_item_in_hand(InteractionHand::OffHand)
135            .has(BLOCKS_ATTACKS)
136}
137
138fn consume_transform_item(
139    item: &mut ItemStack,
140    transform: &BlockTransformData,
141    has_infinite_materials: bool,
142) -> Option<ItemRef> {
143    if item.is_stackable() {
144        if transform.consume_on_use && !has_infinite_materials {
145            item.shrink_one();
146        }
147
148        return None;
149    }
150
151    let item_ref = item.item();
152    item.hurt_and_break(transform.item_damage_per_use, has_infinite_materials)
153        .then_some(item_ref)
154}
155
156fn drop_loot(transform: &BlockTransformData, context: &UseOnContext, old_state: BlockStateId) {
157    let Some(loot) = &transform.loot else {
158        return;
159    };
160
161    let Some(table) = REGISTRY.loot_tables.by_key(loot) else {
162        return;
163    };
164
165    let pos = context.hit_result.block_pos;
166    let tool = context.inv.with_item(|item| item.clone());
167
168    let player: &Player = context.player;
169    let drops = drop_from_block_interact_loot_table(
170        table,
171        old_state,
172        context.world.get_block_entity(pos),
173        Some(&tool),
174        Some(player),
175        &mut rand::rng(),
176    );
177
178    for drop in drops {
179        match transform.drop_strategy {
180            DropStrategy::ClickedFace => {
181                context
182                    .world
183                    .pop_resource_from_face(pos, context.hit_result.direction, drop);
184            }
185            DropStrategy::FromMiddle => {
186                context.world.pop_resource(pos, drop);
187            }
188        }
189    }
190}
191
192const fn particle_event(particle: TransformParticle) -> Option<i32> {
193    match particle {
194        TransformParticle::None => None,
195        TransformParticle::Scrape => Some(level_events::PARTICLES_SCRAPE),
196        TransformParticle::WaxOn => Some(level_events::PARTICLES_WAX_ON),
197        TransformParticle::WaxOff => Some(level_events::PARTICLES_WAX_OFF),
198    }
199}
200
201#[cfg(test)]
202mod tests;