Skip to main content

steel_core/behavior/items/
flint_and_steel.rs

1//! Flint and steel item behavior with portal ignition.
2
3use crate::behavior::blocks::FireBlock;
4use crate::behavior::context::{InteractionResult, UseOnContext};
5use crate::behavior::item::ItemBehavior;
6use steel_macros::item_behavior;
7use steel_registry::item_stack::ItemStack;
8use steel_registry::sound_event::SoundEventRef;
9use steel_registry::vanilla_block_tags::BlockTag;
10use steel_registry::{
11    blocks::{block_state_ext::BlockStateExt, properties::BlockStateProperties},
12    sound_events, vanilla_game_events,
13};
14use steel_utils::types::UpdateFlags;
15use steel_utils::{BlockPos, BlockStateId};
16
17use crate::entity::Entity;
18use crate::world::game_event::GameEventContext;
19
20/// Behavior for flint and steel items.
21#[item_behavior]
22pub struct FlintAndSteelItem;
23
24impl ItemBehavior for FlintAndSteelItem {
25    fn use_on(&self, context: &mut UseOnContext) -> InteractionResult {
26        let click_pos = context.hit_result.block_pos;
27        let clicked_state = context.world.get_block_state(click_pos);
28        if try_light_block(
29            context,
30            click_pos,
31            clicked_state,
32            &sound_events::ITEM_FLINTANDSTEEL_USE,
33            flint_and_steel_pitch(),
34        ) {
35            let has_infinite_materials = context.player.has_infinite_materials();
36            context
37                .inv
38                .with_item(|item| item.hurt_and_break(1, has_infinite_materials));
39            return InteractionResult::Success;
40        }
41
42        let fire_pos = click_pos.relative(context.hit_result.direction);
43        let forward_dir = context.player.direction_yaw();
44
45        if !FireBlock::can_be_placed_at(context.world, fire_pos, forward_dir) {
46            return InteractionResult::Fail;
47        }
48
49        context.world.play_block_sound(
50            &sound_events::ITEM_FLINTANDSTEEL_USE,
51            fire_pos,
52            1.0,
53            rand::random_range(0.8..1.2),
54            Some(context.player.id()),
55        );
56
57        context.world.set_block(
58            fire_pos,
59            FireBlock::get_state(context.world.as_ref(), fire_pos),
60            UpdateFlags::UPDATE_ALL,
61        );
62        context.world.game_event(
63            &vanilla_game_events::BLOCK_PLACE,
64            click_pos,
65            &GameEventContext::new(Some(context.player), None),
66        );
67
68        let has_infinite_materials = context.player.has_infinite_materials();
69        context
70            .inv
71            .with_item(|item| item.hurt_and_break(1, has_infinite_materials));
72
73        InteractionResult::Success
74    }
75}
76
77/// Behavior for fire charge items.
78#[item_behavior]
79pub struct FireChargeItem;
80
81impl ItemBehavior for FireChargeItem {
82    fn use_on(&self, context: &mut UseOnContext) -> InteractionResult {
83        let click_pos = context.hit_result.block_pos;
84        let clicked_state = context.world.get_block_state(click_pos);
85        if try_light_block(
86            context,
87            click_pos,
88            clicked_state,
89            &sound_events::ITEM_FIRECHARGE_USE,
90            fire_charge_pitch(),
91        ) {
92            context.inv.with_item(ItemStack::shrink_one);
93            return InteractionResult::Success;
94        }
95
96        let fire_pos = click_pos.relative(context.hit_result.direction);
97        let forward_dir = context.player.direction_yaw();
98
99        if !FireBlock::can_be_placed_at(context.world, fire_pos, forward_dir) {
100            return InteractionResult::Fail;
101        }
102
103        context.world.play_block_sound(
104            &sound_events::ITEM_FIRECHARGE_USE,
105            fire_pos,
106            1.0,
107            fire_charge_pitch(),
108            Some(context.player.id()),
109        );
110
111        context.world.set_block(
112            fire_pos,
113            FireBlock::get_state(context.world.as_ref(), fire_pos),
114            UpdateFlags::UPDATE_ALL,
115        );
116        context.world.game_event(
117            &vanilla_game_events::BLOCK_PLACE,
118            fire_pos,
119            &GameEventContext::new(Some(context.player), None),
120        );
121
122        context.inv.with_item(ItemStack::shrink_one);
123
124        InteractionResult::Success
125    }
126}
127
128fn try_light_block(
129    context: &UseOnContext<'_>,
130    pos: BlockPos,
131    state: BlockStateId,
132    sound: SoundEventRef,
133    pitch: f32,
134) -> bool {
135    if !can_light(state) {
136        return false;
137    }
138
139    context
140        .world
141        .play_block_sound(sound, pos, 1.0, pitch, Some(context.player.id()));
142    context.world.set_block(
143        pos,
144        state.set_value(&BlockStateProperties::LIT, true),
145        UpdateFlags::UPDATE_ALL_IMMEDIATE,
146    );
147    context.world.game_event(
148        &vanilla_game_events::BLOCK_CHANGE,
149        pos,
150        &GameEventContext::new(Some(context.player), None),
151    );
152
153    true
154}
155
156fn can_light(state: BlockStateId) -> bool {
157    let Some(lit) = state.try_get_value(&BlockStateProperties::LIT) else {
158        return false;
159    };
160    if lit {
161        return false;
162    }
163
164    let block = state.get_block();
165    if block.has_tag(&BlockTag::CAMPFIRES) {
166        return state.try_get_value(&BlockStateProperties::WATERLOGGED) == Some(false);
167    }
168
169    if block.has_tag(&BlockTag::CANDLES) {
170        return state.try_get_value(&BlockStateProperties::WATERLOGGED) == Some(false);
171    }
172
173    block.has_tag(&BlockTag::CANDLE_CAKES)
174}
175
176fn flint_and_steel_pitch() -> f32 {
177    rand::random_range(0.8..1.2)
178}
179
180fn fire_charge_pitch() -> f32 {
181    (rand::random::<f32>() - rand::random::<f32>()) * 0.2 + 1.0
182}
183
184#[cfg(test)]
185mod tests {
186    use steel_registry::{
187        blocks::{block_state_ext::BlockStateExt, properties::BlockStateProperties},
188        init_vanilla_registry, vanilla_blocks,
189    };
190
191    use super::can_light;
192
193    #[test]
194    fn can_light_rejects_waterlogged_campfires_and_candles() {
195        init_vanilla_registry();
196
197        let waterlogged_campfire = vanilla_blocks::CAMPFIRE
198            .default_state()
199            .set_value(&BlockStateProperties::LIT, false)
200            .set_value(&BlockStateProperties::WATERLOGGED, true);
201        let dry_campfire =
202            waterlogged_campfire.set_value(&BlockStateProperties::WATERLOGGED, false);
203
204        let waterlogged_candle = vanilla_blocks::CANDLE
205            .default_state()
206            .set_value(&BlockStateProperties::LIT, false)
207            .set_value(&BlockStateProperties::WATERLOGGED, true);
208        let dry_candle = waterlogged_candle.set_value(&BlockStateProperties::WATERLOGGED, false);
209
210        assert!(!can_light(waterlogged_campfire));
211        assert!(can_light(dry_campfire));
212        assert!(!can_light(waterlogged_candle));
213        assert!(can_light(dry_candle));
214    }
215
216    #[test]
217    fn can_light_accepts_unlit_candle_cakes() {
218        init_vanilla_registry();
219
220        let unlit_candle_cake = vanilla_blocks::CANDLE_CAKE
221            .default_state()
222            .set_value(&BlockStateProperties::LIT, false);
223        let lit_candle_cake = unlit_candle_cake.set_value(&BlockStateProperties::LIT, true);
224
225        assert!(can_light(unlit_candle_cake));
226        assert!(!can_light(lit_candle_cake));
227    }
228}