Skip to main content

steel_core/behavior/blocks/decoration/
candle_cake_block.rs

1use std::sync::Arc;
2
3use glam::DVec3;
4use steel_macros::block_behavior;
5use steel_registry::{
6    blocks::{
7        BlockRef,
8        block_state_ext::BlockStateExt,
9        properties::{BlockStateProperties, BoolProperty},
10    },
11    entity_data::ParticleData,
12    item_stack::ItemStack,
13    items::item::BlockHitResult,
14    sound_events, vanilla_blocks, vanilla_items, vanilla_particle_types,
15};
16use steel_utils::{
17    BlockPos, BlockStateId, Direction,
18    types::{InteractionHand, UpdateFlags},
19};
20
21use crate::{
22    behavior::{
23        BlockBehavior, BlockPlaceContext, InteractionResult, InventoryAccess,
24        blocks::{CakeBlock, CandleBlock},
25    },
26    entity::{Entity, projectile::Projectile},
27    player::Player,
28    world::{ClipHitResult, LevelReader, ScheduledTickAccess, World},
29};
30
31/// Behavior for Candle Cakes
32/// TODO:
33/// - [ ] onExplosion
34#[block_behavior]
35pub struct CandleCakeBlock {
36    block: BlockRef,
37}
38
39const LIT: &BoolProperty = &BlockStateProperties::LIT;
40const PARTICLE_OFFSETS: [DVec3; 1] = [DVec3::new(8.0 * 0.0625, 16.0 * 0.0625, 8.0 * 0.0625)];
41
42impl CandleCakeBlock {
43    /// Creates a new Candle Cake Block Behavior
44    #[must_use]
45    pub const fn new(block: BlockRef) -> Self {
46        Self { block }
47    }
48}
49
50impl BlockBehavior for CandleCakeBlock {
51    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
52        if context
53            .world
54            .get_block_state(context.place_pos().below())
55            .is_solid()
56        {
57            Some(self.block.default_state())
58        } else {
59            None
60        }
61    }
62
63    fn use_item_on(
64        &self,
65        state: BlockStateId,
66        world: &Arc<World>,
67        pos: BlockPos,
68        player: &Player,
69        _hand: InteractionHand,
70        hit_result: &BlockHitResult,
71        inv: &mut InventoryAccess,
72    ) -> InteractionResult {
73        let (is_fire_charge, is_flint_and_steel, is_empty) = inv.with_item(|item_stack| {
74            (
75                item_stack.is(&vanilla_items::FIRE_CHARGE),
76                item_stack.is(&vanilla_items::FLINT_AND_STEEL),
77                item_stack.is_empty(),
78            )
79        });
80        if is_fire_charge || is_flint_and_steel {
81            return InteractionResult::Pass; // lighting of candles and candle cakes is handled by the flint and steel/fire charge implementation
82        } else if (hit_result.location.y - f64::from(hit_result.block_pos.y())) > 0.5
83            && is_empty
84            && state.get_value(LIT)
85        {
86            world.set_block(pos, state.set_value(LIT, false), UpdateFlags::UPDATE_ALL);
87            for particle_pos in PARTICLE_OFFSETS {
88                world.send_particles(
89                    ParticleData::simple(&vanilla_particle_types::SMOKE),
90                    DVec3::new(
91                        f64::from(pos.x()) + particle_pos.x,
92                        f64::from(pos.y()) + particle_pos.y,
93                        f64::from(pos.z()) + particle_pos.z,
94                    ),
95                    1,
96                    DVec3::new(0.0, 0.1, 0.0),
97                    1.0,
98                );
99            }
100            world.play_block_sound(
101                &sound_events::BLOCK_CANDLE_EXTINGUISH,
102                pos,
103                1.0,
104                1.0,
105                Some(player.id()),
106            );
107            return InteractionResult::Success;
108        }
109        InteractionResult::TryEmptyHandInteraction
110    }
111
112    fn use_without_item(
113        &self,
114        state: BlockStateId,
115        world: &Arc<World>,
116        pos: BlockPos,
117        player: &Player,
118        _hit_result: &BlockHitResult,
119        _inv: &mut InventoryAccess,
120    ) -> InteractionResult {
121        let result = CakeBlock::eat(world, pos, vanilla_blocks::CAKE.default_state(), player);
122        if result.consumes_action() {
123            world.drop_resources(state, pos);
124        }
125        result
126    }
127
128    fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
129        world.get_block_state(pos.below()).is_solid()
130    }
131
132    fn update_shape(
133        &self,
134        state: BlockStateId,
135        world: &dyn ScheduledTickAccess,
136        pos: BlockPos,
137        direction: steel_utils::Direction,
138        _neighbor_pos: BlockPos,
139        _neighbor_state: BlockStateId,
140    ) -> BlockStateId {
141        if direction == Direction::Down && !self.can_survive(state, world, pos) {
142            vanilla_blocks::AIR.default_state()
143        } else {
144            state
145        }
146    }
147
148    fn on_projectile_hit(
149        &self,
150        state: BlockStateId,
151        world: &Arc<World>,
152        hit: &ClipHitResult,
153        projectile: &dyn Projectile,
154    ) {
155        let Some(lit_state) = CandleBlock::projectile_lit_state(state, projectile.is_on_fire())
156        else {
157            return;
158        };
159        world.set_block(hit.block_pos, lit_state, UpdateFlags::UPDATE_ALL_IMMEDIATE);
160    }
161
162    fn get_clone_item_stack(
163        &self,
164        _block: BlockRef,
165        _state: BlockStateId,
166        _include_data: bool,
167    ) -> Option<ItemStack> {
168        Some(ItemStack::new(&vanilla_items::CAKE))
169    }
170
171    fn get_analog_output_signal(
172        &self,
173        _state: BlockStateId,
174        _world: &dyn LevelReader,
175        _pos: BlockPos,
176        _direction: Direction,
177    ) -> i32 {
178        CakeBlock::analog_output_signal(0)
179    }
180
181    fn has_analog_output_signal(&self, _state: BlockStateId) -> bool {
182        true
183    }
184}