Skip to main content

steel_core/behavior/items/
dye.rs

1//! Vanilla `DyeItem` behavior: dyes an alive, unsheared sheep and sign text.
2
3use steel_macros::item_behavior;
4use steel_protocol::packets::game::SoundSource;
5use steel_registry::data_components::vanilla_components::DYE;
6use steel_registry::item_stack::ItemStack;
7use steel_registry::sound_events::ITEM_DYE_USE;
8use steel_utils::Downcast as _;
9use steel_utils::types::{InteractionHand, SignTextSlot};
10
11use crate::behavior::{InteractionResult, ItemBehavior, SignApplicator};
12use crate::block_entity::{BlockEntity as _, entities::SignBlockEntity};
13use crate::entity::entities::SheepEntity;
14use crate::entity::{Entity, LivingEntity};
15use crate::player::Player;
16use crate::world::World;
17
18/// Behavior for the sixteen dye items (`DyeItem`).
19///
20/// Ports vanilla `DyeItem.interactLivingEntity` and `DyeItem.tryApplyToSign`:
21/// - Dying a sheep plays `DYE_USE`, sets the sheep's wool color, and consumes one dye.
22/// - Dyeing a sign updates the facing text color and plays `DYE_USE` at `SoundSource::Blocks`.
23#[item_behavior(class = "DyeItem")]
24pub struct DyeItem;
25
26impl ItemBehavior for DyeItem {
27    fn as_sign_applicator(&self) -> Option<&dyn SignApplicator> {
28        Some(self)
29    }
30
31    fn interact_living_entity(
32        &self,
33        stack: &mut ItemStack,
34        _player: &Player,
35        target: &dyn LivingEntity,
36        _hand: InteractionHand,
37    ) -> InteractionResult {
38        let Some(dye_color) = stack.get(DYE).copied() else {
39            return InteractionResult::Pass;
40        };
41        let Some(sheep) = target.downcast_ref::<SheepEntity>() else {
42            return InteractionResult::Pass;
43        };
44
45        if !Entity::is_alive(sheep) || sheep.is_sheared() || sheep.color() == dye_color {
46            return InteractionResult::Pass;
47        }
48
49        sheep.play_sound(&ITEM_DYE_USE, 1.0, 1.0);
50        sheep.set_color(dye_color);
51        stack.shrink_one();
52
53        InteractionResult::Success
54    }
55}
56
57impl SignApplicator for DyeItem {
58    fn try_apply_to_sign(
59        &self,
60        world: &World,
61        sign: &SignBlockEntity,
62        slot: SignTextSlot,
63        item: &ItemStack,
64        _player: &Player,
65    ) -> bool {
66        let Some(dye_color) = item.get(DYE).copied() else {
67            return false;
68        };
69
70        if !sign.set_color(slot, dye_color) {
71            return false;
72        }
73
74        world.play_sound(
75            &ITEM_DYE_USE,
76            SoundSource::Blocks,
77            sign.get_block_pos(),
78            1.0,
79            1.0,
80            None,
81        );
82        true
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use std::sync::Arc;
89
90    use steel_registry::{DyeColor, init_vanilla_registry, vanilla_blocks, vanilla_items};
91    use steel_utils::BlockPos;
92    use text_components::TextComponent;
93
94    use super::*;
95    use crate::block_entity::entities::SignText;
96    use crate::test_support::{TestPlayerBuilder, fresh_test_world};
97
98    #[test]
99    fn dye_sign_applicator_updates_color_and_respects_existing_color() {
100        init_vanilla_registry();
101        let world_fixture = fresh_test_world("dye_sign_applicator");
102        let world = &world_fixture.world;
103        let sign = SignBlockEntity::new(
104            Arc::downgrade(world),
105            BlockPos::new(0, 64, 0),
106            vanilla_blocks::OAK_SIGN.default_state(),
107        );
108
109        let mut dye_stack = ItemStack::new(&vanilla_items::RED_DYE);
110        dye_stack.set(DYE, DyeColor::Red);
111
112        let player = TestPlayerBuilder::new(Arc::clone(world), "SignDyer", 1).build();
113        let applicator = DyeItem;
114
115        assert!(applicator.try_apply_to_sign(
116            world,
117            &sign,
118            SignTextSlot::Front,
119            &dye_stack,
120            &player
121        ));
122        assert_eq!(sign.get_text(SignTextSlot::Front).color, DyeColor::Red);
123        assert!(!applicator.try_apply_to_sign(
124            world,
125            &sign,
126            SignTextSlot::Front,
127            &dye_stack,
128            &player
129        ));
130
131        let mut empty_text = SignText::new();
132        assert!(!applicator.can_apply_to_sign(&empty_text, &dye_stack, &player));
133
134        empty_text.set_message(0, TextComponent::plain("Hello"));
135        assert!(applicator.can_apply_to_sign(&empty_text, &dye_stack, &player));
136    }
137}