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