steel_core/behavior/items/
block_transformer.rs1use 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 ®ISTRY,
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 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;